@tea-agent/loop-agent 0.39.0-next.3 → 0.39.0-next.30
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/AGENTS.md +4 -1
- package/CHANGELOG.md +267 -65
- package/README.md +11 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +428 -27
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +118 -14
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +8 -1
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +533 -29
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/operator/capabilities.js +181 -27
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +22 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +478 -55
- package/dist/worker/console/chat/semantic-activity.js +20 -8
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/chat/turn-process.js +17 -30
- package/dist/worker/console/harness-pi-model-matrix.js +137 -0
- package/dist/worker/console/operator-actions.js +244 -4
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +12 -5
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +83 -10
- package/dist/worker/console/server.js +42 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-CiGOV5eG.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-Ds9Gyfd8.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-C6V6uC98.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DezF7Ol9.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BCWk3gCT.js} +1 -1
- package/dist/worker/console/static/assets/channel-DMvu1nmi.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DbYr-Cxy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-XSlusW4G.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-BtcKAj9i.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BF57lhXj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-DJIlR25o.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-DklNc7dm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-fZNtEdQZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-BBgLqV4h.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-Dl9e5n_J.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-B0JK51NT.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVzM-02m.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVzM-02m.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-Ch-ajv4O.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-xn_NxcfB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DRD0fyga.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-COtnKp4B.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-BQp_CAqG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-x0y-vNgq.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-N2ezwL31.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-CSaxRH6Q.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-6Ro7Kpbd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-CrIIQEAI.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-DivKcq8g.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-BXrfDo8y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-LTTQmMRJ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-DP3o_5qd.js} +1 -1
- package/dist/worker/console/static/assets/index-C2jmYBfu.js +404 -0
- package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-wVfSY1WH.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-BRKfFL0I.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DEml3jLb.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-DTzBmOqN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-Cpy1d873.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-C1cSavOy.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-BEBSKvYM.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BrLs4Oac.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-B47AZcLa.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-s0dVeYR2.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-lW8aJ7Q6.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-BcHJPf6Q.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-DdegpUUf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-BxFWqNv8.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-cOdUDPbR.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-DpxlYl8P.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BwecGAkM.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DQOfurE0.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D5WW_LbN.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-CIp3tC6V.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-C-adIPKn.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CiYrar9M.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-WHrdTV0-.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +39 -0
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +3 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +493 -2
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +221 -31
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/init-hybrid.js +805 -113
- package/dist/workflows/dag/node-execution.js +194 -6
- package/dist/workflows/dag/prompt.js +60 -1
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +224 -27
- package/dist/workflows/dag/rerun-run.js +61 -6
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +113 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +157 -1
- package/dist/workflows/dag/validate.js +41 -1
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-implementation-contract.schema.json +9 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -1
- package/package.json +12 -4
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/frontend-design-review/references/review-checklist.md +2 -0
- package/skills/frontend-implementation/references/design-spec.md +16 -19
- package/skills/frontend-implementation/references/node-contracts.md +9 -6
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/** Shown near the preview header when window.open is blocked or Blob/URL creation fails. */
|
|
2
|
+
export const PREVIEW_OPEN_POPUP_BLOCKED = "无法在浏览器中打开:弹窗被拦截";
|
|
3
|
+
export const PREVIEW_TRUNCATION_NOTICE = "内容已截断,以下为已加载片段。";
|
|
4
|
+
/** Window features that force a real OS window instead of a browser tab.
|
|
5
|
+
* Do not include `noopener` here: browsers then return null even on success. */
|
|
6
|
+
export function previewOpenWindowFeatures(input) {
|
|
7
|
+
const availWidth = input?.availWidth ?? 1440;
|
|
8
|
+
const availHeight = input?.availHeight ?? 900;
|
|
9
|
+
const width = Math.min(1280, Math.max(880, Math.floor(availWidth * 0.82)));
|
|
10
|
+
const height = Math.min(960, Math.max(640, Math.floor(availHeight * 0.88)));
|
|
11
|
+
return [
|
|
12
|
+
"popup=yes",
|
|
13
|
+
`width=${width}`,
|
|
14
|
+
`height=${height}`,
|
|
15
|
+
"left=72",
|
|
16
|
+
"top=36",
|
|
17
|
+
"menubar=yes",
|
|
18
|
+
"toolbar=yes",
|
|
19
|
+
"location=yes",
|
|
20
|
+
"status=yes",
|
|
21
|
+
"resizable=yes",
|
|
22
|
+
"scrollbars=yes",
|
|
23
|
+
].join(",");
|
|
24
|
+
}
|
|
25
|
+
export function escapeHtml(text) {
|
|
26
|
+
return text
|
|
27
|
+
.replaceAll("&", "&")
|
|
28
|
+
.replaceAll("<", "<")
|
|
29
|
+
.replaceAll(">", ">")
|
|
30
|
+
.replaceAll('"', """)
|
|
31
|
+
.replaceAll("'", "'");
|
|
32
|
+
}
|
|
33
|
+
function normalizedLanguage(language) {
|
|
34
|
+
return language.trim().toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
export function isHtmlPreviewLanguage(language) {
|
|
37
|
+
const lang = normalizedLanguage(language);
|
|
38
|
+
return lang === "html" || lang === "htm";
|
|
39
|
+
}
|
|
40
|
+
export function isMarkdownPreviewLanguage(language) {
|
|
41
|
+
const lang = normalizedLanguage(language);
|
|
42
|
+
return lang === "md" || lang === "markdown";
|
|
43
|
+
}
|
|
44
|
+
function formatTextBody(preview) {
|
|
45
|
+
if (normalizedLanguage(preview.language) !== "json")
|
|
46
|
+
return preview.content;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.stringify(JSON.parse(preview.content), null, 2);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return preview.content;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isSafeHttpHref(href) {
|
|
55
|
+
return /^https?:\/\//i.test(href.trim());
|
|
56
|
+
}
|
|
57
|
+
function renderMarkdownInline(text) {
|
|
58
|
+
const withCode = [];
|
|
59
|
+
const coded = text.replace(/`([^`]+)`/g, (_all, code) => {
|
|
60
|
+
withCode.push(`<code>${code}</code>`);
|
|
61
|
+
return `\u0000${withCode.length - 1}\u0000`;
|
|
62
|
+
});
|
|
63
|
+
const linked = coded.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_all, label, href) => {
|
|
64
|
+
if (!isSafeHttpHref(href))
|
|
65
|
+
return label;
|
|
66
|
+
return `<a href="${href}" rel="noopener noreferrer" target="_blank">${label}</a>`;
|
|
67
|
+
});
|
|
68
|
+
const emphasized = linked
|
|
69
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
70
|
+
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
|
71
|
+
.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
72
|
+
return emphasized.replace(/\u0000(\d+)\u0000/g, (_all, idx) => {
|
|
73
|
+
return withCode[Number(idx)] ?? "";
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function flushParagraph(lines) {
|
|
77
|
+
if (!lines.length)
|
|
78
|
+
return "";
|
|
79
|
+
return `<p>${renderMarkdownInline(lines.join(" "))}</p>`;
|
|
80
|
+
}
|
|
81
|
+
function renderMarkdownBlocks(escaped) {
|
|
82
|
+
const lines = escaped.replaceAll("\r\n", "\n").split("\n");
|
|
83
|
+
const out = [];
|
|
84
|
+
let paragraph = [];
|
|
85
|
+
let listKind = null;
|
|
86
|
+
const closeList = () => {
|
|
87
|
+
if (listKind) {
|
|
88
|
+
out.push(listKind === "ul" ? "</ul>" : "</ol>");
|
|
89
|
+
listKind = null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const closeParagraph = () => {
|
|
93
|
+
if (paragraph.length) {
|
|
94
|
+
out.push(flushParagraph(paragraph));
|
|
95
|
+
paragraph = [];
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
|
|
100
|
+
if (heading) {
|
|
101
|
+
closeParagraph();
|
|
102
|
+
closeList();
|
|
103
|
+
const level = heading[1].length;
|
|
104
|
+
out.push(`<h${level}>${renderMarkdownInline(heading[2])}</h${level}>`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (/^---+$/.test(line.trim()) || /^\*\*\*+$/.test(line.trim())) {
|
|
108
|
+
closeParagraph();
|
|
109
|
+
closeList();
|
|
110
|
+
out.push("<hr>");
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const quote = /^>\s?(.*)$/.exec(line);
|
|
114
|
+
if (quote) {
|
|
115
|
+
closeParagraph();
|
|
116
|
+
closeList();
|
|
117
|
+
out.push(`<blockquote><p>${renderMarkdownInline(quote[1] ?? "")}</p></blockquote>`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const ul = /^[-*]\s+(.+)$/.exec(line);
|
|
121
|
+
if (ul) {
|
|
122
|
+
closeParagraph();
|
|
123
|
+
if (listKind !== "ul") {
|
|
124
|
+
closeList();
|
|
125
|
+
out.push("<ul>");
|
|
126
|
+
listKind = "ul";
|
|
127
|
+
}
|
|
128
|
+
out.push(`<li>${renderMarkdownInline(ul[1])}</li>`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const ol = /^\d+\.\s+(.+)$/.exec(line);
|
|
132
|
+
if (ol) {
|
|
133
|
+
closeParagraph();
|
|
134
|
+
if (listKind !== "ol") {
|
|
135
|
+
closeList();
|
|
136
|
+
out.push("<ol>");
|
|
137
|
+
listKind = "ol";
|
|
138
|
+
}
|
|
139
|
+
out.push(`<li>${renderMarkdownInline(ol[1])}</li>`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (line.trim() === "") {
|
|
143
|
+
closeParagraph();
|
|
144
|
+
closeList();
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
closeList();
|
|
148
|
+
paragraph.push(line);
|
|
149
|
+
}
|
|
150
|
+
closeParagraph();
|
|
151
|
+
closeList();
|
|
152
|
+
return out.join("\n");
|
|
153
|
+
}
|
|
154
|
+
/** Conservative Markdown → HTML for the detached reading tab. Input is escaped first. */
|
|
155
|
+
export function renderMarkdownLite(source) {
|
|
156
|
+
const escaped = escapeHtml(source);
|
|
157
|
+
const chunks = [];
|
|
158
|
+
const fence = /```([^\n`]*)\n([\s\S]*?)```/g;
|
|
159
|
+
let last = 0;
|
|
160
|
+
let match;
|
|
161
|
+
while ((match = fence.exec(escaped))) {
|
|
162
|
+
if (match.index > last) {
|
|
163
|
+
chunks.push(renderMarkdownBlocks(escaped.slice(last, match.index)));
|
|
164
|
+
}
|
|
165
|
+
chunks.push(`<pre class="md-code"><code>${match[2] ?? ""}</code></pre>`);
|
|
166
|
+
last = match.index + match[0].length;
|
|
167
|
+
}
|
|
168
|
+
if (last < escaped.length) {
|
|
169
|
+
chunks.push(renderMarkdownBlocks(escaped.slice(last)));
|
|
170
|
+
}
|
|
171
|
+
return chunks.filter(Boolean).join("\n");
|
|
172
|
+
}
|
|
173
|
+
function readingPageCss() {
|
|
174
|
+
return `
|
|
175
|
+
:root { color-scheme: light dark; }
|
|
176
|
+
html, body { margin: 0; min-height: 100%; background: #f7f5f0; color: #1f1b16; }
|
|
177
|
+
body { font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
178
|
+
.bar { position: sticky; top: 0; z-index: 1; display: flex; gap: 12px; align-items: center; padding: 10px 16px 10px 20px; border-bottom: 1px solid #e4ddd2; background: #fffdf8; }
|
|
179
|
+
.bar .meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
|
180
|
+
.bar strong { font-size: 13px; font-weight: 650; word-break: break-all; }
|
|
181
|
+
.bar .kind { color: #6b6258; font-size: 12px; }
|
|
182
|
+
.modes { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid #e4ddd2; border-radius: 8px; background: #f3eee6; }
|
|
183
|
+
.modes button { appearance: none; border: 0; background: transparent; color: #6b6258; font: 12px/1.2 inherit; padding: 5px 10px; border-radius: 6px; cursor: pointer; }
|
|
184
|
+
.modes button:hover { color: #1f1b16; }
|
|
185
|
+
.modes button.is-active { background: #fff; color: #1f1b16; font-weight: 600; box-shadow: 0 1px 2px rgb(0 0 0 / 8%); }
|
|
186
|
+
.notice { margin: 0; padding: 8px 20px; background: #fff6d8; color: #5c4b00; font-size: 13px; }
|
|
187
|
+
.pane { display: none; }
|
|
188
|
+
.pane.is-active { display: block; }
|
|
189
|
+
.doc { max-width: 52rem; margin: 0 auto; padding: 32px 24px 96px; }
|
|
190
|
+
.doc.md h1, .doc.md h2, .doc.md h3, .doc.md h4 { line-height: 1.25; margin: 1.4em 0 0.5em; }
|
|
191
|
+
.doc.md h1 { font-size: 2rem; }
|
|
192
|
+
.doc.md h2 { font-size: 1.45rem; }
|
|
193
|
+
.doc.md p { margin: 0.8em 0; }
|
|
194
|
+
.doc.md ul, .doc.md ol { margin: 0.6em 0; padding-left: 1.4em; }
|
|
195
|
+
.doc.md blockquote { margin: 0.8em 0; padding: 0.2em 0.9em; border-left: 3px solid #d9cfc0; color: #4a433c; }
|
|
196
|
+
.doc.md hr { border: 0; border-top: 1px solid #e4ddd2; margin: 1.6em 0; }
|
|
197
|
+
.doc.md a { color: #0b57d0; }
|
|
198
|
+
.doc.md code, .md-code, .source { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.92em; }
|
|
199
|
+
.doc.md :not(pre) > code { padding: 0.1em 0.35em; border-radius: 4px; background: #efe8dc; }
|
|
200
|
+
.md-code, .source { margin: 1em 0; padding: 14px 16px; overflow: auto; white-space: pre-wrap; background: #fff; border: 1px solid #e4ddd2; border-radius: 8px; }
|
|
201
|
+
.frame { margin: 0; padding: 0; }
|
|
202
|
+
iframe.page { display: block; width: 100%; height: calc(100vh - 53px); border: 0; background: #fff; }
|
|
203
|
+
`.trim();
|
|
204
|
+
}
|
|
205
|
+
function formattedSource(preview) {
|
|
206
|
+
return escapeHtml(formatTextBody(preview));
|
|
207
|
+
}
|
|
208
|
+
function previewPane(preview) {
|
|
209
|
+
if (isHtmlPreviewLanguage(preview.language)) {
|
|
210
|
+
return `<div class="frame"><iframe class="page" sandbox="" srcdoc="${escapeHtml(preview.content)}"></iframe></div>`;
|
|
211
|
+
}
|
|
212
|
+
if (isMarkdownPreviewLanguage(preview.language)) {
|
|
213
|
+
return `<article class="doc md">${renderMarkdownLite(preview.content)}</article>`;
|
|
214
|
+
}
|
|
215
|
+
return `<article class="doc"><pre class="source">${formattedSource(preview)}</pre></article>`;
|
|
216
|
+
}
|
|
217
|
+
function sourcePane(preview) {
|
|
218
|
+
return `<article class="doc"><pre class="source">${formattedSource(preview)}</pre></article>`;
|
|
219
|
+
}
|
|
220
|
+
function readingPageScript() {
|
|
221
|
+
return `<script>
|
|
222
|
+
(function () {
|
|
223
|
+
var previewBtn = document.getElementById("view-preview");
|
|
224
|
+
var sourceBtn = document.getElementById("view-source");
|
|
225
|
+
var previewPane = document.getElementById("pane-preview");
|
|
226
|
+
var sourcePane = document.getElementById("pane-source");
|
|
227
|
+
if (!previewBtn || !sourceBtn || !previewPane || !sourcePane) return;
|
|
228
|
+
function show(mode) {
|
|
229
|
+
var source = mode === "source";
|
|
230
|
+
previewBtn.classList.toggle("is-active", !source);
|
|
231
|
+
sourceBtn.classList.toggle("is-active", source);
|
|
232
|
+
previewBtn.setAttribute("aria-pressed", String(!source));
|
|
233
|
+
sourceBtn.setAttribute("aria-pressed", String(source));
|
|
234
|
+
previewPane.classList.toggle("is-active", !source);
|
|
235
|
+
sourcePane.classList.toggle("is-active", source);
|
|
236
|
+
}
|
|
237
|
+
previewBtn.addEventListener("click", function () { show("preview"); });
|
|
238
|
+
sourceBtn.addEventListener("click", function () { show("source"); });
|
|
239
|
+
})();
|
|
240
|
+
</script>`;
|
|
241
|
+
}
|
|
242
|
+
/** Build the Blob document HTML from the already-loaded preview snapshot. */
|
|
243
|
+
export function buildPreviewBrowserDocument(preview) {
|
|
244
|
+
const title = escapeHtml(preview.path);
|
|
245
|
+
const notice = preview.truncated
|
|
246
|
+
? `<p class="notice">${escapeHtml(PREVIEW_TRUNCATION_NOTICE)}</p>`
|
|
247
|
+
: "";
|
|
248
|
+
const lang = normalizedLanguage(preview.language);
|
|
249
|
+
return `<!doctype html>
|
|
250
|
+
<html lang="zh-CN">
|
|
251
|
+
<head>
|
|
252
|
+
<meta charset="utf-8">
|
|
253
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
254
|
+
<title>${title}</title>
|
|
255
|
+
<style>
|
|
256
|
+
${readingPageCss()}
|
|
257
|
+
</style>
|
|
258
|
+
</head>
|
|
259
|
+
<body>
|
|
260
|
+
<header class="bar">
|
|
261
|
+
<div class="meta">
|
|
262
|
+
<strong>${title}</strong>
|
|
263
|
+
<span class="kind">${escapeHtml(lang || "text")}${preview.truncated ? " · 已截断" : ""}</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="modes" role="group" aria-label="查看模式">
|
|
266
|
+
<button type="button" id="view-preview" class="is-active" aria-pressed="true">预览</button>
|
|
267
|
+
<button type="button" id="view-source" aria-pressed="false">查看原文</button>
|
|
268
|
+
</div>
|
|
269
|
+
</header>
|
|
270
|
+
${notice}
|
|
271
|
+
<div id="pane-preview" class="pane is-active">${previewPane(preview)}</div>
|
|
272
|
+
<div id="pane-source" class="pane">${sourcePane(preview)}</div>
|
|
273
|
+
${readingPageScript()}
|
|
274
|
+
</body>
|
|
275
|
+
</html>`;
|
|
276
|
+
}
|
|
277
|
+
function screenAvail() {
|
|
278
|
+
const screen = globalThis.screen;
|
|
279
|
+
return {
|
|
280
|
+
availWidth: screen?.availWidth,
|
|
281
|
+
availHeight: screen?.availHeight,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function defaultHost() {
|
|
285
|
+
const g = globalThis;
|
|
286
|
+
if (typeof g.open !== "function") {
|
|
287
|
+
throw new Error("window.open is unavailable");
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
open: g.open.bind(g),
|
|
291
|
+
createObjectURL: g.URL.createObjectURL.bind(g.URL),
|
|
292
|
+
revokeObjectURL: g.URL.revokeObjectURL.bind(g.URL),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Open the current preview in a new OS window from already-loaded
|
|
297
|
+
* path/content/language/truncated. Does not fetch the repo file API again.
|
|
298
|
+
*
|
|
299
|
+
* Width/height window features ask the browser for a real window, not a tab.
|
|
300
|
+
* Do not pass `noopener` as a feature: browsers then return null even on
|
|
301
|
+
* success. Detach via `opener = null` after a real handle is obtained. Keep
|
|
302
|
+
* the Blob URL alive — revoking immediately blanks the page.
|
|
303
|
+
*/
|
|
304
|
+
export function openPreviewInBrowser(preview, host = defaultHost(), features = previewOpenWindowFeatures(screenAvail())) {
|
|
305
|
+
let objectUrl;
|
|
306
|
+
try {
|
|
307
|
+
const documentHtml = buildPreviewBrowserDocument(preview);
|
|
308
|
+
const blob = new Blob([documentHtml], { type: "text/html;charset=utf-8" });
|
|
309
|
+
objectUrl = host.createObjectURL(blob);
|
|
310
|
+
const opened = host.open(objectUrl, "_blank", features);
|
|
311
|
+
if (!opened) {
|
|
312
|
+
host.revokeObjectURL(objectUrl);
|
|
313
|
+
return { ok: false, error: PREVIEW_OPEN_POPUP_BLOCKED };
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
opened.opener = null;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// Some hosts expose a frozen handle; the window is still open.
|
|
320
|
+
}
|
|
321
|
+
return { ok: true };
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
if (objectUrl)
|
|
325
|
+
host.revokeObjectURL(objectUrl);
|
|
326
|
+
return { ok: false, error: PREVIEW_OPEN_POPUP_BLOCKED };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource-panel diagnostics presentation.
|
|
3
|
+
*
|
|
4
|
+
* Pi ResourceLoader emits machine English (`warning` / `collision` + full
|
|
5
|
+
* Windows paths). The Skills / Packages / Models panels must not dump that
|
|
6
|
+
* block above the inventory — it pushes the list off-screen. Classify here,
|
|
7
|
+
* then render a header chip + grouped popover with natural Chinese copy.
|
|
8
|
+
*/
|
|
9
|
+
const COLLISION_NAME = /name\s+"([^"]+)"\s+collision/i;
|
|
10
|
+
const INVALID_CHARS = /invalid characters|must be lowercase/i;
|
|
11
|
+
const INVALID_EDGE_HYPHEN = /must not start or end with a hyphen/i;
|
|
12
|
+
const INVALID_DOUBLE_HYPHEN = /consecutive hyphens/i;
|
|
13
|
+
const GROUP_COPY = {
|
|
14
|
+
"load-failed": {
|
|
15
|
+
heading: "需要处理",
|
|
16
|
+
hint: "有些清单没读完整,下面的列表可能不全。",
|
|
17
|
+
},
|
|
18
|
+
"invalid-name": {
|
|
19
|
+
heading: "名称不规范",
|
|
20
|
+
hint: "技能已经加载,还能用。名称请改成小写字母、数字和连字符,例如 sdd-requirement-analysis。",
|
|
21
|
+
},
|
|
22
|
+
collision: {
|
|
23
|
+
heading: "同名未生效",
|
|
24
|
+
hint: "同名技能只保留先扫到的那一份,后面的不会进当前会话。",
|
|
25
|
+
},
|
|
26
|
+
other: {
|
|
27
|
+
heading: "其他提示",
|
|
28
|
+
hint: "不影响你浏览当前列表。",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const GROUP_ORDER = [
|
|
32
|
+
"load-failed",
|
|
33
|
+
"invalid-name",
|
|
34
|
+
"collision",
|
|
35
|
+
"other",
|
|
36
|
+
];
|
|
37
|
+
export function shortResourcePath(path) {
|
|
38
|
+
if (!path)
|
|
39
|
+
return undefined;
|
|
40
|
+
const parts = path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
41
|
+
if (parts.length === 0)
|
|
42
|
+
return path;
|
|
43
|
+
const file = parts.at(-1);
|
|
44
|
+
if (!file)
|
|
45
|
+
return path;
|
|
46
|
+
const parent = parts.at(-2);
|
|
47
|
+
return parent ? `${parent}/${file}` : file;
|
|
48
|
+
}
|
|
49
|
+
export function skillNameFromPath(path) {
|
|
50
|
+
if (!path)
|
|
51
|
+
return undefined;
|
|
52
|
+
const parts = path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
53
|
+
if (parts.length === 0)
|
|
54
|
+
return undefined;
|
|
55
|
+
const last = parts.at(-1);
|
|
56
|
+
if (!last)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (/^skill\.md$/i.test(last) && parts.length >= 2) {
|
|
59
|
+
return parts.at(-2);
|
|
60
|
+
}
|
|
61
|
+
return last.replace(/\.md$/i, "");
|
|
62
|
+
}
|
|
63
|
+
export function diagnosticMatchesSkill(item, skill) {
|
|
64
|
+
if (item.path) {
|
|
65
|
+
const left = item.path.replace(/\\/g, "/").toLowerCase();
|
|
66
|
+
const right = skill.path.replace(/\\/g, "/").toLowerCase();
|
|
67
|
+
if (left === right || left.endsWith(right) || right.endsWith(left)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return Boolean(item.skillName && item.skillName === skill.name);
|
|
72
|
+
}
|
|
73
|
+
function shortenMessage(message) {
|
|
74
|
+
const trimmed = message.trim();
|
|
75
|
+
if (trimmed.length <= 80)
|
|
76
|
+
return trimmed;
|
|
77
|
+
return `${trimmed.slice(0, 79)}…`;
|
|
78
|
+
}
|
|
79
|
+
function titleForLoadFailed(diag) {
|
|
80
|
+
const hay = `${diag.code ?? ""} ${diag.message}`.toLowerCase();
|
|
81
|
+
if (hay.includes("preference"))
|
|
82
|
+
return "自动使用偏好读不出来";
|
|
83
|
+
if (hay.includes("package"))
|
|
84
|
+
return "软件包清单没读完整";
|
|
85
|
+
if (hay.includes("skill"))
|
|
86
|
+
return "技能清单没读完整";
|
|
87
|
+
if (hay.includes("extension"))
|
|
88
|
+
return "扩展没读完整";
|
|
89
|
+
if (hay.includes("prompt"))
|
|
90
|
+
return "提示词没读完整";
|
|
91
|
+
if (hay.includes("theme"))
|
|
92
|
+
return "主题没读完整";
|
|
93
|
+
if (hay.includes("context"))
|
|
94
|
+
return "上下文文件没读完整";
|
|
95
|
+
return "有些内容没读完整";
|
|
96
|
+
}
|
|
97
|
+
function detailForInvalidName(message) {
|
|
98
|
+
if (INVALID_DOUBLE_HYPHEN.test(message)) {
|
|
99
|
+
return "名称里不要连用两个连字符。现在这样也能用。";
|
|
100
|
+
}
|
|
101
|
+
if (INVALID_EDGE_HYPHEN.test(message)) {
|
|
102
|
+
return "名称开头和结尾不要用连字符。现在这样也能用。";
|
|
103
|
+
}
|
|
104
|
+
return "名称请只用小写字母、数字和连字符。现在这样也能用。";
|
|
105
|
+
}
|
|
106
|
+
export function classifyResourceDiagnostic(diag) {
|
|
107
|
+
const type = (diag.type ?? "warning").toLowerCase();
|
|
108
|
+
const message = diag.message ?? "";
|
|
109
|
+
const collisionMatch = message.match(COLLISION_NAME) ??
|
|
110
|
+
(type === "collision" ? message.match(/"([^"]+)"/) : null);
|
|
111
|
+
if (type === "collision" || collisionMatch) {
|
|
112
|
+
const skillName = collisionMatch?.[1] ?? skillNameFromPath(diag.path);
|
|
113
|
+
return {
|
|
114
|
+
kind: "collision",
|
|
115
|
+
tone: "warning",
|
|
116
|
+
title: skillName ? `「${skillName}」有多份` : "有一份同名技能没生效",
|
|
117
|
+
detail: "同名只保留先扫到的那一份,这份被盖掉了。",
|
|
118
|
+
path: diag.path,
|
|
119
|
+
shortPath: shortResourcePath(diag.path),
|
|
120
|
+
skillName,
|
|
121
|
+
retryable: false,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (INVALID_CHARS.test(message) ||
|
|
125
|
+
INVALID_EDGE_HYPHEN.test(message) ||
|
|
126
|
+
INVALID_DOUBLE_HYPHEN.test(message)) {
|
|
127
|
+
const skillName = skillNameFromPath(diag.path);
|
|
128
|
+
return {
|
|
129
|
+
kind: "invalid-name",
|
|
130
|
+
tone: "warning",
|
|
131
|
+
title: skillName ?? "有个技能名称不合规范",
|
|
132
|
+
detail: detailForInvalidName(message),
|
|
133
|
+
path: diag.path,
|
|
134
|
+
shortPath: shortResourcePath(diag.path),
|
|
135
|
+
skillName,
|
|
136
|
+
retryable: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const isLoadFailed = type === "error" ||
|
|
140
|
+
diag.retryable === true ||
|
|
141
|
+
diag.code === "RESOURCE_LOAD_FAILED" ||
|
|
142
|
+
diag.code === "PACKAGE_INVENTORY_FAILED" ||
|
|
143
|
+
diag.code === "PI_RESOURCE_PREFERENCES_MALFORMED";
|
|
144
|
+
if (isLoadFailed) {
|
|
145
|
+
return {
|
|
146
|
+
kind: "load-failed",
|
|
147
|
+
tone: "error",
|
|
148
|
+
title: titleForLoadFailed(diag),
|
|
149
|
+
detail: shortenMessage(message),
|
|
150
|
+
path: diag.path,
|
|
151
|
+
shortPath: shortResourcePath(diag.path),
|
|
152
|
+
retryable: diag.retryable === true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
kind: "other",
|
|
157
|
+
tone: type === "error" ? "error" : "warning",
|
|
158
|
+
title: shortenMessage(message) || "一条提示",
|
|
159
|
+
path: diag.path,
|
|
160
|
+
shortPath: shortResourcePath(diag.path),
|
|
161
|
+
retryable: false,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export function summarizeResourceDiagnostics(diagnostics) {
|
|
165
|
+
const items = diagnostics.map(classifyResourceDiagnostic);
|
|
166
|
+
const buckets = new Map();
|
|
167
|
+
for (const item of items) {
|
|
168
|
+
const list = buckets.get(item.kind) ?? [];
|
|
169
|
+
list.push(item);
|
|
170
|
+
buckets.set(item.kind, list);
|
|
171
|
+
}
|
|
172
|
+
const groups = GROUP_ORDER.flatMap((kind) => {
|
|
173
|
+
const groupItems = buckets.get(kind);
|
|
174
|
+
if (!groupItems?.length)
|
|
175
|
+
return [];
|
|
176
|
+
return [{ kind, ...GROUP_COPY[kind], items: groupItems }];
|
|
177
|
+
});
|
|
178
|
+
const errorCount = items.filter((item) => item.tone === "error").length;
|
|
179
|
+
const warningCount = items.length - errorCount;
|
|
180
|
+
return {
|
|
181
|
+
total: items.length,
|
|
182
|
+
errorCount,
|
|
183
|
+
warningCount,
|
|
184
|
+
chipLabel: chipLabel(errorCount, warningCount),
|
|
185
|
+
groups,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function chipLabel(errorCount, warningCount) {
|
|
189
|
+
if (errorCount > 0 && warningCount > 0) {
|
|
190
|
+
return `${errorCount} 个问题 · ${warningCount} 条提示`;
|
|
191
|
+
}
|
|
192
|
+
if (errorCount > 0) {
|
|
193
|
+
return errorCount === 1 ? "1 个问题" : `${errorCount} 个问题`;
|
|
194
|
+
}
|
|
195
|
+
if (warningCount === 1)
|
|
196
|
+
return "1 条提示";
|
|
197
|
+
return `${warningCount} 条提示`;
|
|
198
|
+
}
|
|
199
|
+
export function skillHasNameWarning(skill, items) {
|
|
200
|
+
return items.some((item) => item.kind === "invalid-name" && diagnosticMatchesSkill(item, skill));
|
|
201
|
+
}
|
|
202
|
+
export function noticesForSkill(skill, items) {
|
|
203
|
+
return items.filter((item) => item.kind !== "collision" && diagnosticMatchesSkill(item, skill));
|
|
204
|
+
}
|
|
@@ -31,6 +31,13 @@ export function operatorChatEventsUrl(origin, sessionId) {
|
|
|
31
31
|
export function operatorChatAbortUrl(origin, sessionId, turnId) {
|
|
32
32
|
return `${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/turns/${encodeURIComponent(turnId)}/abort`;
|
|
33
33
|
}
|
|
34
|
+
export function turnPhaseFromOwnership(bag) {
|
|
35
|
+
if (bag.stoppingTurnId || bag.stopRequested)
|
|
36
|
+
return "stopping";
|
|
37
|
+
if (bag.activeTurnId || bag.streaming || bag.submitInFlight || bag.pendingSubmission)
|
|
38
|
+
return "running";
|
|
39
|
+
return "idle";
|
|
40
|
+
}
|
|
34
41
|
export function createTurnOwnershipBag(seed) {
|
|
35
42
|
return {
|
|
36
43
|
submitInFlight: false,
|
|
@@ -53,7 +60,7 @@ export function createTurnOwnershipBag(seed) {
|
|
|
53
60
|
}
|
|
54
61
|
/** Whether a second concurrent send is blocked by ownership / busy. */
|
|
55
62
|
export function isSubmitOwnershipBusy(bag) {
|
|
56
|
-
return bag
|
|
63
|
+
return turnPhaseFromOwnership(bag) !== "idle";
|
|
57
64
|
}
|
|
58
65
|
/**
|
|
59
66
|
* Release submit ownership. Optionally scoped to a clientRequestId so a stale
|
|
@@ -3,6 +3,48 @@
|
|
|
3
3
|
* Extracted so double-submit, TURN_ACTIVE adoption, transport ambiguity, and
|
|
4
4
|
* generation fences can be unit-tested without mounting the full React tree.
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Choose the submission path from the mutable ownership facts, never from a
|
|
8
|
+
* possibly stale React render. A stopping intent without a bound Turn must
|
|
9
|
+
* remain held until reconciliation binds or releases that ownership.
|
|
10
|
+
*/
|
|
11
|
+
export function decideSubmissionRoute(input) {
|
|
12
|
+
if (input.turnPhase === "stopping") {
|
|
13
|
+
return input.blockedByTurnId
|
|
14
|
+
? {
|
|
15
|
+
action: "queue-deferred",
|
|
16
|
+
blockedByTurnId: input.blockedByTurnId,
|
|
17
|
+
}
|
|
18
|
+
: { action: "hold" };
|
|
19
|
+
}
|
|
20
|
+
return input.turnPhase === "idle" ? { action: "send-now" } : { action: "hold" };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reconcile a deferred PUT that raced with Stop completion. The same
|
|
24
|
+
* clientRequestId may move to the normal POST path only after authoritative
|
|
25
|
+
* state proves that no Turn/runtime remains busy.
|
|
26
|
+
*/
|
|
27
|
+
export function reconcileDeferredSubmissionFromState(input) {
|
|
28
|
+
const { clientRequestId, blockedByTurnId, state } = input;
|
|
29
|
+
if (!state)
|
|
30
|
+
return { action: "hold" };
|
|
31
|
+
if (state.deferredTurn?.clientRequestId === clientRequestId) {
|
|
32
|
+
return { action: "deferred", deferredTurn: state.deferredTurn };
|
|
33
|
+
}
|
|
34
|
+
if (state.activeTurn?.clientRequestId === clientRequestId) {
|
|
35
|
+
return { action: "accepted", turn: state.activeTurn };
|
|
36
|
+
}
|
|
37
|
+
if (state.latestTurn?.clientRequestId === clientRequestId) {
|
|
38
|
+
return { action: "accepted", turn: state.latestTurn };
|
|
39
|
+
}
|
|
40
|
+
const activeTurnId = state.activeTurn?.turnId ?? state.activeTurnId ?? null;
|
|
41
|
+
if (state.turnPhase === "stopping" && activeTurnId === blockedByTurnId) {
|
|
42
|
+
return { action: "retry-deferred" };
|
|
43
|
+
}
|
|
44
|
+
if (!isRuntimeBusyUnion(state))
|
|
45
|
+
return { action: "send-now" };
|
|
46
|
+
return { action: "hold" };
|
|
47
|
+
}
|
|
6
48
|
export function createPendingSubmission(input) {
|
|
7
49
|
let resolveOutcome;
|
|
8
50
|
const outcomePromise = new Promise((resolve) => {
|
|
@@ -5,6 +5,7 @@ import { hydrateSessionThread } from "../../chat/turn-process.js";
|
|
|
5
5
|
import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
|
|
6
6
|
import { CHAT_SESSION_LIST_LIMIT } from "../chat-view-types.js";
|
|
7
7
|
import { confirmationToken } from "./format.js";
|
|
8
|
+
import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
|
|
8
9
|
import { SessionTitleWatcher } from "./session-title-watcher.js";
|
|
9
10
|
export async function createSessionAndPrimeTitleWatcher(input) {
|
|
10
11
|
if (input.creatingRef?.current) {
|
|
@@ -69,6 +70,8 @@ export function useChatSessions(params) {
|
|
|
69
70
|
// Shared busy gate for sidebar + Landing create entries (one render feedback).
|
|
70
71
|
const [creatingSession, setCreatingSession] = useState(false);
|
|
71
72
|
const creatingSessionRef = useRef(false);
|
|
73
|
+
/** One-shot guard so a stale-token failure schedules a single page reload. */
|
|
74
|
+
const compactReloadScheduledRef = useRef(false);
|
|
72
75
|
// Only the latest activate/create/restore intent may commit active-session
|
|
73
76
|
// state. This prevents slow responses from an older selection overwriting a
|
|
74
77
|
// newer user choice.
|
|
@@ -215,6 +218,9 @@ export function useChatSessions(params) {
|
|
|
215
218
|
messages: hydrated.messages,
|
|
216
219
|
toolCalls: hydrated.toolCalls,
|
|
217
220
|
streaming: Boolean(body.session.activeTurnId),
|
|
221
|
+
turnPhase: body.session.turnPhase ??
|
|
222
|
+
(body.session.activeTurnId ? "running" : "idle"),
|
|
223
|
+
deferredTurn: body.session.deferredTurn ?? null,
|
|
218
224
|
});
|
|
219
225
|
window.localStorage.setItem(ACTIVE_CHAT_SESSION_STORAGE_KEY, sessionId);
|
|
220
226
|
setActivePreviewPath(null);
|
|
@@ -396,6 +402,18 @@ export function useChatSessions(params) {
|
|
|
396
402
|
});
|
|
397
403
|
const body = (await response.json().catch(() => ({})));
|
|
398
404
|
if (!response.ok) {
|
|
405
|
+
const gateCode = body.error?.code;
|
|
406
|
+
if (isMutationGateFailureCode(gateCode)) {
|
|
407
|
+
// Stale boot tokens: the page predates the current server boot.
|
|
408
|
+
// Auto-reload once so the fresh boot page re-injects tokens; keep
|
|
409
|
+
// an error line up while reloading in case reload is blocked.
|
|
410
|
+
setError(describeMutationGateFailure(gateCode));
|
|
411
|
+
if (isStaleTokenFailure(gateCode) && !compactReloadScheduledRef.current) {
|
|
412
|
+
compactReloadScheduledRef.current = true;
|
|
413
|
+
window.setTimeout(() => window.location.reload(), 1200);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
399
417
|
setError(body.error?.message ?? "上下文压缩失败");
|
|
400
418
|
return;
|
|
401
419
|
}
|