diff --git a/Agent.md b/Agent.md index c68028fe..49443817 100644 --- a/Agent.md +++ b/Agent.md @@ -121,7 +121,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg Python: `uv run pytest tests/ -v` (1148) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (95: 45 daemon_client + 20 conn-manager + 8 integration + 6 build-config + 7 gui-state + 3 preload-api + 4 boot-contract + 2 theme-guard) — syntax: `node --check main.js preload.js daemon_client.js` -Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (449: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 11 markdown + 15 transcript + 7 TranscriptView + 15 history + 22 composer + 14 Composer + 12 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 27 workspaceView + 10 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 9 openSession + 6 WelcomeDialog + 8 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 15 daemonBridge + 7 DaemonBridgeProvider + 26 Shell + 15 DialogHost + 20 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown) + `npm run build` → `renderer/dist/` +Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (451: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 11 markdown + 15 transcript + 8 TranscriptView + 15 history + 22 composer + 15 Composer + 12 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 27 workspaceView + 10 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 9 openSession + 6 WelcomeDialog + 8 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 15 daemonBridge + 7 DaemonBridgeProvider + 26 Shell + 15 DialogHost + 20 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown) + `npm run build` → `renderer/dist/` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响) Git-over-https 兜底: `python scripts/sync-master-from-api.py [--repo owner/name] [--ref master]` — 受限网络下 github.com:443 不可达而 api.github.com 可达时,用 Git Data API 的 verification payload + signature 字节级重建上游 commit(含 web-flow GPG 签名 squash merge,reconstruct_commit 经 hermetic 测试验证 sha 一致)并推进本地 refs;内容对象缺失时 fail-loud 提示改用 git fetch(10+ 周期实证的恢复路径) diff --git a/emrg/gui/renderer/css/layout.css b/emrg/gui/renderer/css/layout.css index 044e69e7..0006a1ea 100644 --- a/emrg/gui/renderer/css/layout.css +++ b/emrg/gui/renderer/css/layout.css @@ -342,7 +342,8 @@ box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow-md); } -#input { +#input, +.tiptap-input { flex: 1; border: none; background: transparent; @@ -351,10 +352,38 @@ max-height: 150px; line-height: 1.6; padding: var(--sp-2) 0; + outline: none; + overflow-y: auto; + font-family: inherit; + font-size: inherit; + color: var(--text-1); + caret-color: var(--accent); } -#input:focus { +#input:focus, +.tiptap-input:focus, +.tiptap-input:focus-visible { border: none; box-shadow: none; + outline: none; +} + +/* tiptap 内容可编辑区内部段落:默认 p 有 margin,输入框内收紧 */ +.tiptap-input .ProseMirror { + min-height: 1.6em; +} +.tiptap-input .ProseMirror p { + margin: 0; +} +.tiptap-input .ProseMirror p.is-editor-empty:first-child { + position: relative; +} +.tiptap-input .ProseMirror p.is-editor-empty:first-child::before { + position: absolute; + left: 0; + top: 0; + content: attr(data-placeholder); + color: var(--text-3); + pointer-events: none; } .composer-hint { diff --git a/emrg/gui/renderer/package-lock.json b/emrg/gui/renderer/package-lock.json index f9913e67..80b36444 100644 --- a/emrg/gui/renderer/package-lock.json +++ b/emrg/gui/renderer/package-lock.json @@ -8,8 +8,14 @@ "name": "emrg-gui-renderer", "version": "0.1.0", "dependencies": { + "@tiptap/extension-link": "^3.30.5", + "@tiptap/extension-placeholder": "^3.30.5", + "@tiptap/pm": "^3.30.5", + "@tiptap/react": "^3.30.5", + "@tiptap/starter-kit": "^3.30.5", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "tiptap-markdown": "^0.9.0" }, "devDependencies": { "@testing-library/jest-dom": "^7.0.1", @@ -272,6 +278,34 @@ } } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT", + "optional": true + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -675,6 +709,449 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tiptap/core": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.30.5.tgz", + "integrity": "sha512-3O7N0FyKIfuLV+xrdWyDM3V5eUY/q2CgLjhhMwOAbM1Pu7VPp9VP+TpEYOdH8aRyB+h1vj5hX5A747D8ZrPfHA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.30.5" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.30.5.tgz", + "integrity": "sha512-8pf1ZDrl6XlVPUee/2YlWZPFSF7yqsJEX6WLW41x06MT1dapG3Qudcns0znj6kYsX8JXTJ+Mhegy4z19bvTtRg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5", + "@tiptap/pm": "3.30.5" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.30.5.tgz", + "integrity": "sha512-MLZS+s/BJiJbv2C2M3G4FQGn43kPwYUUr2TiW3afSn+dRkEZlDxx5CwZYbe1PEt2+VX/SIifn945Oqr3s0axSA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.30.5.tgz", + "integrity": "sha512-redcmBInVwmipjF/WEVcNwCUJgJygUv8leidfRb/jBSAwx7GgnQWPr2HVQDk9zeTt3ykWld2NRdrk94Crjymow==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5", + "@tiptap/pm": "3.30.5" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.30.5.tgz", + "integrity": "sha512-66OGw4suO0Gr/4QAEiSvVi0eSvEqStvu1moHUSvUlwvEqnuB0ME2w9HuRogPElzWdrYLzbBA47mlCi9Veva0ew==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.30.5" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.30.5.tgz", + "integrity": "sha512-0dCt8eBo4sMtw+LjUu+0GF0JrTlREROdWoy8TM5kFPxJ5ZU2LKDiROXDPwXStaaoFFR/aStLH9xSpdz7cHUiUA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.30.5.tgz", + "integrity": "sha512-SvkNoOio2xBy9AtSMyFKMp88MTUvyIXsGCnuz71INV2wHdH4VfRPoQLT6e077LpTCa9w6LHyTKtbZA4HYOp7wg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5", + "@tiptap/pm": "3.30.5" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.30.5.tgz", + "integrity": "sha512-4mKoD3bBr5W2AQ2Y/7amOqcVw0eqJbUwRtwvxypeo5Hi0PB9O5Ev8P05c3EUTo10bllqdKYXGeCU5AcgdQsHWQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.30.5.tgz", + "integrity": "sha512-gJxn9PMUee8zazQBYqbOZiI7zTFXPVJ15AzTnLfUu4eDtPieMwtpgcjhCt4bZlp37drLm+Li76wWLirb7iLxsw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.30.5" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.30.5.tgz", + "integrity": "sha512-gTjGPWUpGn8IoW533TciZJZC/81LmBgU7zee+aRMRF3ix3K2z1pJjl6knDvrRQA6Kom+yWFaiueqGNXkrk38XQ==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.30.5", + "@tiptap/pm": "3.30.5" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.30.5.tgz", + "integrity": "sha512-h3m2ZA1XXLAfdHwdtG0MZnf0KWbNyP8xTI3RUlTDR5apmKmYVBaeocOJwTcFFZ92gPKqxYKyhteZqYtHOCZGJA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.30.5" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.30.5.tgz", + "integrity": "sha512-PcL4Z8l/DlauwZUJg0jf6SXKk6/YOXJtd8yYzqHrmCLRoY4kAa7+TxUhc/lWhuitUiOZ8+nB8V/NNM+WR6mRaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.30.5.tgz", + "integrity": "sha512-x7e7+p1bWXvwz6vqONPP1SVB73V0gZP7LIDL/5KMnxWLjahMTabgVPgjV30kz7GsOEgUYtEf7eCyGVGNNWL5Dg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.30.5.tgz", + "integrity": "sha512-s5t2xM6wPYRJl2cqYplc7Ze8m8xddk4DP2v0aLFcUuD100D1B5eJbxsiwLK6wBAzNwiuVjd486RnmUpcswaXhQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5", + "@tiptap/pm": "3.30.5" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.30.5.tgz", + "integrity": "sha512-Fu9EuSRlHQNGES7UhY4mQod3yUKnWwxsGlPuDJURfwju/Ug04Jz8iRuERZBPcRgLNkwKDmbDsWUZF72RRIL2Uw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.30.5.tgz", + "integrity": "sha512-zsQ+q83HpCYOMTNzdjb12dggE7sZMp0aqQJhArdavSTl8hLbVY/u3qJN88t25IEwAKVK0pcTJfWWA2QFl8bjAQ==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5", + "@tiptap/pm": "3.30.5" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.30.5.tgz", + "integrity": "sha512-CHThihH+7TA0TfwtmA+eTXP1MAsA/+p011olJHg8Rilj5xA+L+0IFkjhN16JnZkBgSA4MnfEv15UMNfRaNpuVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5", + "@tiptap/pm": "3.30.5" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.30.5.tgz", + "integrity": "sha512-N0fKUyQkPQBvVB48imjfIfdexU9VsAX1RydajYP2xAt+QOJ8KHVmXv1EGTp+v7pGdZMLdls1cSZ2f352vW3aWQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.30.5" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.30.5.tgz", + "integrity": "sha512-3pLR2yo29uhowaGMbD8v71XpgdNmqotBg1NXlyTMVFzxELj+VjjeLLTqSguR/iWDUbHXNzIsfMEgpRnNMO//8w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.30.5" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.30.5.tgz", + "integrity": "sha512-bUGUnSAgjZhoUWBtr+1wRJHF3NnNvuKQr6sQ8le1tJ2yvLq448ZsSZjZGvTNeLsy3GDKBoMJ0rqLUK34+nM3hg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.30.5" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.30.5.tgz", + "integrity": "sha512-GrNNlAImfQhYRtGE95YNAjcTclUUMPHs9JeE9vMgfcoKcOmZ6GsbWUdN0hA55xqwGaUjnroOVtf77K9z1EbkJQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.30.5.tgz", + "integrity": "sha512-WgkX4ImAAeV+Qe7ATJ+Gio2MmKWON1CKpCKPHWgCObaoMEaSmdoxXPmI9wXMuM+AJydJepp4n7U/V4TVts14rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.30.5" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.30.5.tgz", + "integrity": "sha512-r8IbWUm4YXNCkmc6h6dzuREQIGbO0x9z9l3GfQPUR3LxFtGssDLngE4dFXHRt/ejLWKEauy6clklf7LawtvYzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.30.5.tgz", + "integrity": "sha512-pOgj4mIGFlw4NUdA6PCTFP/MHrYWnOYiZRYeu0I3/Yo3iN4Am/CgYBLl8PWsNh+YPO2hA2DWs7ZMke9D2j5R+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.30.5.tgz", + "integrity": "sha512-u12G/WW2uFRY95BzrSAU9RW002K+7lFpSCL6fb7Puh8yLhek3lddOtAx0P+NI/PyMKtcoVMtBxoi+0mwVM7NPQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.30.5.tgz", + "integrity": "sha512-5x3OiCYBvXz0G5OGM8f7ka++1dh9EXdNRZ8IcMcEd+QwW4RPwvmJ2EjheA1eRRyMlcbNU7T+4IRlBXURlTetEA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5", + "@tiptap/pm": "3.30.5" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.30.5.tgz", + "integrity": "sha512-gufkLkW2tA6PZPjivYxDiGzTIIftwqhmYI6lvvKu2S4FbhcysJgMAe/GXVSywzCRVVex9SrvCe6RFYrqnwRitQ==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.4.1", + "prosemirror-commands": "^1.7.1", + "prosemirror-dropcursor": "^1.8.2", + "prosemirror-gapcursor": "^1.4.1", + "prosemirror-history": "^1.5.0", + "prosemirror-inputrules": "^1.5.1", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.11", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.5", + "prosemirror-transform": "^1.12.0", + "prosemirror-view": "^1.41.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.30.5.tgz", + "integrity": "sha512-QtHdOmYTCMRkCf5dmLtIqFq0e/yxavkQ6ZXbtytoftWEKJf3MjBTlX4nVAgpvrLAjPmCTxiehEXXxD1AJm/Syw==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.30.5", + "@tiptap/extension-floating-menu": "^3.30.5" + }, + "peerDependencies": { + "@tiptap/core": "3.30.5", + "@tiptap/pm": "3.30.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.30.5", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.30.5.tgz", + "integrity": "sha512-cmDRvukpoqjQjhE96q+l3tCLNZZcJKehxK+00Sp9F4XbKecfM2QpZ2TRHh3vi8qhCeDQNyMoCqfNaqznIjLjIQ==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "3.30.5", + "@tiptap/extension-blockquote": "3.30.5", + "@tiptap/extension-bold": "3.30.5", + "@tiptap/extension-bullet-list": "3.30.5", + "@tiptap/extension-code": "3.30.5", + "@tiptap/extension-code-block": "3.30.5", + "@tiptap/extension-document": "3.30.5", + "@tiptap/extension-dropcursor": "3.30.5", + "@tiptap/extension-gapcursor": "3.30.5", + "@tiptap/extension-hard-break": "3.30.5", + "@tiptap/extension-heading": "3.30.5", + "@tiptap/extension-horizontal-rule": "3.30.5", + "@tiptap/extension-italic": "3.30.5", + "@tiptap/extension-link": "3.30.5", + "@tiptap/extension-list": "3.30.5", + "@tiptap/extension-list-item": "3.30.5", + "@tiptap/extension-list-keymap": "3.30.5", + "@tiptap/extension-ordered-list": "3.30.5", + "@tiptap/extension-paragraph": "3.30.5", + "@tiptap/extension-strike": "3.30.5", + "@tiptap/extension-text": "3.30.5", + "@tiptap/extension-underline": "3.30.5", + "@tiptap/extensions": "3.30.5", + "@tiptap/pm": "3.30.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -708,11 +1185,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "13.0.9", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz", + "integrity": "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^3", + "@types/mdurl": "^1" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -722,12 +1220,17 @@ "version": "19.2.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript/typescript-aix-ppc64": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", @@ -1236,6 +1739,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -1308,7 +1817,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/data-urls": { @@ -1415,6 +1923,15 @@ "node": ">=12.0.0" } }, + "node_modules/fast-equals": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", + "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1800,6 +2317,31 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.5.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", @@ -1831,6 +2373,51 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-it": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.1.tgz", + "integrity": "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "license": "ISC" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/mdn-data": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", @@ -1838,6 +2425,12 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -1881,6 +2474,12 @@ "node": ">=12.20.0" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -1966,6 +2565,178 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/prosemirror-changeset": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.2.tgz", + "integrity": "sha512-ViYrjMSg3YFiXwIhKaluu+/mi3Yrxt6AR8ri14ulTaGcZtXO1CThl7A2gv79qx5fQnOw8woKwyBU2u+9PVCm3w==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz", + "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz", + "integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.6.tgz", + "integrity": "sha512-dY6g2BXRjkHW2ldNRDKfTF0x0R4ifk5rgPaABd/UhvrymtsGVxTbHn01goEsHAvO9nN0O34cttlW1qg/XUcaIg==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-markdown/node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/prosemirror-markdown/node_modules/@types/markdown-it": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/prosemirror-markdown/node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, + "node_modules/prosemirror-model": { + "version": "1.25.11", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz", + "integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.3", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.3.tgz", + "integrity": "sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.8", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1976,6 +2747,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", @@ -2063,6 +2843,12 @@ "@rolldown/binding-win32-x64-msvc": "1.2.5" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -2177,6 +2963,24 @@ "node": ">=14.0.0" } }, + "node_modules/tiptap-markdown": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.9.0.tgz", + "integrity": "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==", + "license": "MIT", + "workspaces": [ + "example" + ], + "dependencies": { + "@types/markdown-it": "^13.0.7", + "markdown-it": "^14.1.0", + "markdown-it-task-lists": "^2.1.1", + "prosemirror-markdown": "^1.11.1" + }, + "peerDependencies": { + "@tiptap/core": "^3.0.1" + } + }, "node_modules/tldts": { "version": "7.4.11", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", @@ -2258,6 +3062,12 @@ "@typescript/typescript-win32-x64": "7.0.2" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/undici": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", @@ -2268,6 +3078,15 @@ "node": ">=22.19.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", @@ -2436,6 +3255,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/emrg/gui/renderer/package.json b/emrg/gui/renderer/package.json index de1cf833..ee8532c9 100644 --- a/emrg/gui/renderer/package.json +++ b/emrg/gui/renderer/package.json @@ -12,8 +12,14 @@ "test:watch": "vitest" }, "dependencies": { + "@tiptap/extension-link": "^3.30.5", + "@tiptap/extension-placeholder": "^3.30.5", + "@tiptap/pm": "^3.30.5", + "@tiptap/react": "^3.30.5", + "@tiptap/starter-kit": "^3.30.5", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "tiptap-markdown": "^0.9.0" }, "devDependencies": { "@testing-library/jest-dom": "^7.0.1", diff --git a/emrg/gui/renderer/src/components/Composer.test.tsx b/emrg/gui/renderer/src/components/Composer.test.tsx index 9bed3fa0..78d5a9c8 100644 --- a/emrg/gui/renderer/src/components/Composer.test.tsx +++ b/emrg/gui/renderer/src/components/Composer.test.tsx @@ -1,14 +1,17 @@ import { describe, expect, it } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; +import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import type { Editor } from "@tiptap/react"; +import { useRef, type MutableRefObject } from "react"; import { Composer, type SendResult } from "./Composer"; import { createTranscriptStore, type TranscriptStore } from "../lib/transcript"; import { I18nProvider } from "../lib/i18n"; /** - * Composer.test.tsx — 输入框 + / 补全菜单 + 发送流组件测试(Batch 2 remainder)。 - * 注入假 sendMessage,断言:补全菜单出现/键盘导航/选择填充、发送流程(用户行 + - * 输入清空 + requestId)、失败恢复(G49:文案 + 文本回填)、busy 队列注入(#655)。 + * Composer.test.tsx — 输入框 + / 补全菜单 + 发送流组件测试(tiptap 版,rant 2026-08-28T14:07:29)。 + * 迁移:userEvent.type textarea → tiptap editor 命令驱动(editorRef.commands.insertContent / + * editor.view.dom 键盘事件)。注入假 sendMessage,断言:补全菜单出现/键盘导航/选择填充、 + * 发送流程(用户行 + 输入清空 + requestId)、失败恢复(G49)、busy 队列注入(#655)。 */ function setup( @@ -20,20 +23,44 @@ function setup( onCommand?: (r: { type: "command" | "unknown"; cmd: string; args?: string[] }) => void; } = {}, ) { + const editorRef: MutableRefObject = { current: null }; const utils = render( - + , ); - const input = () => screen.getByTestId("composer-input") as HTMLTextAreaElement; - return { ...utils, input }; + const editor = () => editorRef.current; + const input = () => screen.getByTestId("composer-input") as HTMLElement; + const type = (text: string) => { + act(() => { + editorRef.current?.commands.insertContent(text); + }); + }; + /** 在编辑器 DOM 上派发真实 keydown(tiptap editorProps.handleKeyDown 拦截) */ + const press = (key: string, mods: { shiftKey?: boolean; ctrlKey?: boolean } = {}) => { + act(() => { + const dom = editorRef.current?.view.dom; + if (!dom) throw new Error("editor not mounted"); + dom.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true, ...mods })); + }); + }; + return { ...utils, editor, input, type, press }; +} + +/** 等待 editorRef 挂载(immediatelyRender:false → useEffect 后才有实例) */ +async function waitEditor(setupResult: ReturnType) { + await waitFor(() => expect(setupResult.editor()).not.toBeNull()); + return setupResult.editor()!; } describe("Composer — 补全菜单", () => { it("输入 / 前缀弹出匹配菜单(无空格仍在指令词)", async () => { const store = createTranscriptStore(); - const { input } = setup(store); - await userEvent.type(input(), "/c"); + const s = setup(store); + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("/c"); + }); const menu = screen.getByTestId("cmd-menu"); expect(menu).toBeInTheDocument(); expect(screen.getAllByRole("menuitem").length).toBeGreaterThan(0); @@ -41,52 +68,67 @@ describe("Composer — 补全菜单", () => { it("空格后输入内容关闭菜单(离开指令词 → 普通消息)", async () => { const store = createTranscriptStore(); - const { input } = setup(store); + const s = setup(store); + const editor = await waitEditor(s); // 注意:vanilla 用 trim() 判定——尾部空格被去后仍是指令词,菜单保留;输词后关闭 - await userEvent.type(input(), "/clear x"); + act(() => { + editor.commands.insertContent("/clear x"); + }); expect(screen.queryByTestId("cmd-menu")).not.toBeInTheDocument(); }); it("↑↓ 导航切换选中项(vanilla (index±1+n)%n 环绕)", async () => { const store = createTranscriptStore(); - const { input } = setup(store); - await userEvent.type(input(), "/c"); + const s = setup(store); + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("/c"); + }); const items = screen.getAllByRole("menuitem"); expect(items[0]).toHaveClass("selected"); - await userEvent.keyboard("{ArrowDown}"); + s.press("ArrowDown"); expect(items[1]).toHaveClass("selected"); expect(items[0]).not.toHaveClass("selected"); // 环绕:最后一项 + ↓ → 回到第 0 项((last+1) % n = 0) - await userEvent.keyboard("{ArrowDown}"); + s.press("ArrowDown"); expect(items[0]).toHaveClass("selected"); // ↑ 也环绕:第 0 项 + ↑ → 回最后一项((0-1+n) % n = n-1) - await userEvent.keyboard("{ArrowUp}"); + s.press("ArrowUp"); expect(items[items.length - 1]).toHaveClass("selected"); }); it("Enter 选择补全项 → 填充输入框并关闭菜单(用户可继续回车执行)", async () => { const store = createTranscriptStore(); - const { input } = setup(store); - await userEvent.type(input(), "/clear"); - await userEvent.keyboard("{Enter}"); - expect(input().value).toBe("/clear"); + const s = setup(store); + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("/clear"); + }); + s.press("Enter"); + expect(editor.getText()).toBe("/clear"); expect(screen.queryByTestId("cmd-menu")).not.toBeInTheDocument(); }); it("mousedown 点击补全项 → 填充输入框(preventDefault 防失焦)", async () => { const store = createTranscriptStore(); - const { input } = setup(store); - await userEvent.type(input(), "/clear"); + const s = setup(store); + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("/clear"); + }); await userEvent.click(screen.getByTestId("cmd-item-/clear")); - expect(input().value).toBe("/clear"); + expect(editor.getText()).toBe("/clear"); }); it("Escape 关闭菜单", async () => { const store = createTranscriptStore(); - const { input } = setup(store); - await userEvent.type(input(), "/c"); + const s = setup(store); + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("/c"); + }); expect(screen.getByTestId("cmd-menu")).toBeInTheDocument(); - await userEvent.keyboard("{Escape}"); + s.press("Escape"); expect(screen.queryByTestId("cmd-menu")).not.toBeInTheDocument(); }); }); @@ -95,19 +137,22 @@ describe("Composer — 发送流", () => { it("Enter 发送消息:用户行入 store + 输入清空 + sendMessage 带预生成 requestId + sandbox", async () => { const store = createTranscriptStore(); const sent: Array<{ text: string; requestId: string; sandbox?: string | null }> = []; - const { input } = setup(store, { + const s = setup(store, { sendMessage: async (o) => { sent.push({ text: o.text, requestId: o.requestId, sandbox: o.sandbox }); return { requestId: o.requestId }; }, }); - await userEvent.type(input(), "hello emrg"); - await userEvent.keyboard("{Enter}"); - expect(sent).toHaveLength(1); + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("hello emrg"); + }); + s.press("Enter"); + await waitFor(() => expect(sent).toHaveLength(1)); expect(sent[0].text).toBe("hello emrg"); expect(sent[0].sandbox).toBe("workspace-write"); expect(sent[0].requestId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); - expect(input().value).toBe(""); + expect(editor.getText()).toBe(""); const entries = store.getEntries("s1"); expect(entries[entries.length - 1]).toMatchObject({ kind: "user", text: "hello emrg" }); }); @@ -115,73 +160,89 @@ describe("Composer — 发送流", () => { it("Ctrl+Enter 同样发送", async () => { const store = createTranscriptStore(); let sent = 0; - setup(store, { + const s = setup(store, { sendMessage: async () => { sent++; return {}; }, }); - await userEvent.type(screen.getByTestId("composer-input"), "ctrl send"); - await userEvent.keyboard("{Control>}{Enter}{/Control}"); - expect(sent).toBe(1); + await waitEditor(s); + s.type("ctrl send"); + s.press("Enter", { ctrlKey: true }); + await waitFor(() => expect(sent).toBe(1)); }); it("Shift+Enter 不发送(换行)", async () => { const store = createTranscriptStore(); let sent = 0; - const { input } = setup(store, { + const s = setup(store, { sendMessage: async () => { sent++; return {}; }, }); - await userEvent.type(input(), "line one"); - await userEvent.keyboard("{Shift>}{Enter}{/Shift}"); + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("line one"); + }); + s.press("Enter", { shiftKey: true }); expect(sent).toBe(0); + // Shift+Enter → hardBreak 换行(tiptap 默认),内容保留(getText 以 \n 表示 hardBreak) + expect(editor.getText()).toBe("line one\n"); }); it("空输入不发送", async () => { const store = createTranscriptStore(); let sent = 0; - setup(store, { + const s = setup(store, { sendMessage: async () => { sent++; return {}; }, }); - await userEvent.keyboard("{Enter}"); + await waitEditor(s); + s.press("Enter"); expect(sent).toBe(0); }); it("失败恢复:系统消息(copy.sendFailed)+ 文本回填", async () => { const store = createTranscriptStore(); - const { input } = setup(store, { + const s = setup(store, { sendMessage: async () => { throw new Error("boom"); }, }); - await userEvent.type(input(), "will fail"); - await userEvent.keyboard("{Enter}"); - // I18nProvider lang=zh → 译文;await 异步失败分支(G49:恢复输入框) - expect( - store.getEntries("s1").some((e) => e.kind === "system" && e.text === "没发送成功,你的话我还留着,再试一次?"), - ).toBe(true); - await waitFor(() => expect(input().value).toBe("will fail")); + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("will fail"); + }); + s.press("Enter"); + // 异步失败分支(G49:恢复输入框)——await sendFn 的 rejection 在微任务里, + // 系统消息断言必须 waitFor + await waitFor(() => + expect( + store.getEntries("s1").some((e) => e.kind === "system" && e.text === "没发送成功,你的话我还留着,再试一次?"), + ).toBe(true), + ); + await waitFor(() => expect(editor.getText()).toBe("will fail")); }); it("无会话时提示 app.needSession 且不发送", async () => { const store = createTranscriptStore(); let sent = 0; - setup(store, { + const s = setup(store, { sid: null, sendMessage: async () => { sent++; return {}; }, }); - await userEvent.type(screen.getByTestId("composer-input"), "orphan"); - await userEvent.keyboard("{Enter}"); - expect(sent).toBe(0); + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("orphan"); + }); + s.press("Enter"); + await waitFor(() => expect(sent).toBe(0)); expect(store.getEntries(null).some((e) => e.kind === "system" && e.text === "请先创建一个对话。")).toBe(true); }); @@ -189,33 +250,59 @@ describe("Composer — 发送流", () => { const store = createTranscriptStore(); let sent = 0; const routed: Array<{ cmd: string; args?: string[] }> = []; - const { input } = setup(store, { + const s = setup(store, { sendMessage: async () => { sent++; return {}; }, onCommand: (r) => routed.push({ cmd: r.cmd, args: r.args }), }); - await userEvent.type(input(), "/clear now"); - await userEvent.keyboard("{Enter}"); + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("/clear now"); + }); + s.press("Enter"); + await waitFor(() => expect(routed).toEqual([{ cmd: "/clear", args: ["now"] }])); expect(sent).toBe(0); - expect(routed).toEqual([{ cmd: "/clear", args: ["now"] }]); - expect(input().value).toBe(""); + expect(editor.getText()).toBe(""); + }); + + it("markdown round-trip:setContent(md) 回显不损坏(rant 验收:旧纯文本/富文本编辑场景)", async () => { + const store = createTranscriptStore(); + const s = setup(store, { + sendMessage: async () => ({}), + }); + const editor = await waitEditor(s); + // 旧纯文本消息 → setContent 解析 → 文本不变 + act(() => { + editor.commands.setContent("plain old message"); + }); + expect(editor.getText()).toBe("plain old message"); + // markdown 富文本 → getMarkdown 序列化等价(history.jsonl 存 markdown str) + act(() => { + editor.commands.setContent("**bold** and `code`"); + }); + const md = (editor.storage as unknown as { markdown: { getMarkdown(): string } }).markdown.getMarkdown(); + expect(md.trim()).toContain("**bold**"); + expect(md.trim()).toContain("`code`"); }); it("busy 时发送 → 入队(queue-injection #655)并在成功时以 daemon 回显 requestId 为准", async () => { const store = createTranscriptStore(); const sent: string[] = []; - const { input } = setup(store, { + const s = setup(store, { busy: true, sendMessage: async (o) => { sent.push(o.requestId); return { requestId: `echo-${o.requestId}` }; }, }); - await userEvent.type(input(), "queued msg"); - await userEvent.keyboard("{Enter}"); - expect(sent).toHaveLength(1); // busy 不拦截——直接发送 + const editor = await waitEditor(s); + act(() => { + editor.commands.insertContent("queued msg"); + }); + s.press("Enter"); + await waitFor(() => expect(sent).toHaveLength(1)); // busy 不拦截——直接发送 // G124:ownStream 以 daemon 回显为准 const version = store.getVersion(); void version; diff --git a/emrg/gui/renderer/src/components/Composer.tsx b/emrg/gui/renderer/src/components/Composer.tsx index bcd93761..e0b43ac3 100644 --- a/emrg/gui/renderer/src/components/Composer.tsx +++ b/emrg/gui/renderer/src/components/Composer.tsx @@ -1,4 +1,8 @@ -import { useEffect, useRef, useState, type KeyboardEvent } from "react"; +import { useEffect, useRef, useState, type MutableRefObject } from "react"; +import { EditorContent, useEditor, type Editor } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import Placeholder from "@tiptap/extension-placeholder"; +import { Markdown } from "tiptap-markdown"; import { parseInput } from "../lib/commands"; import { CMD_MENU_CLOSED, @@ -12,19 +16,19 @@ import { useI18n } from "../lib/i18n"; import type { TranscriptStore } from "../lib/transcript"; /** - * Composer — 输入框 + / 补全菜单 + 发送流(Batch 2 remainder,蓝图 cycle-155056)。 - * 源:vanilla renderer/js/app.js sendMessage(131-170)+ CommandMenu(480-526)+ - * keydown 导航(~1710-1735)+ P2 queue-injection(#655)。 + * Composer — 输入框 + / 补全菜单 + 发送流(tiptap 版,rant 2026-08-28T14:07:29 Stage 1)。 * - * - 纯逻辑在 lib/composer.ts / lib/history.ts / lib/commands.ts(可单测); - * 本组件负责 DOM 接线(textarea 自适应高度 ≤150px、键盘导航、mousedown 选择)。 - * - 发送流:parseInput → / 指令走 onCommand 回调(Batch 5 由 App 路由到对话框); - * message → busy 队列注入(requestId 预生成、wasBusy 时入 queuedSends)。 + * Stage 1 = 编辑器核心替换:textarea → @tiptap/react 内容可编辑编辑器 + * (StarterKit + Link + Placeholder + tiptap-markdown 序列化)。 + * - 序列化(rant 核心原则):发送时 editor.storage.markdown.getMarkdown() 出 markdown + * 字符串,走现有 sendMessage 路径。history.jsonl / 协议层 / TUI 零改动; + * 存储仍是 markdown str,双端渲染不变。 + * - 保留现有功能:Enter=发送 / Shift+Enter=换行(hardBreak)/ Ctrl+Enter=发送、 + * / 补全菜单(editor.onUpdate + keydown 拦截)、高度自适应 ≤150px(CSS max-height)、 + * G143 requestId 预生成、P2 busy 队列注入(#655)、失败恢复(setContent 回填 markdown)。 + * - Stage 2(后续 cycle):mini 格式栏 + 格式快捷键 + DOMPurify 粘贴净化。 * - sendMessage 注入式(测试传假实现;默认 window.emrg.sendMessage)。 - * - busy 为受控可选 prop:Batch 5 接线后由 daemon 广播(done/cancelled/error)驱动; - * 未受控时组件内部管理(发送置忙、失败复位)。 - * - 类名与 vanilla 一致(composer-card / cmd-menu / cmd-menu-item / send-btn), - * Batch 5 切换复用现有 CSS。 + * - 类名与 vanilla 一致(composer-card / cmd-menu / cmd-menu-item / send-btn)。 */ export interface SendOptions { @@ -53,6 +57,8 @@ export interface ComposerProps { sendMessage?: (opts: SendOptions) => Promise; /** / 指令路由回调(Batch 5 接线:/clear /model /memory …) */ onCommand?: (routing: CommandRouting) => void; + /** 测试注入:挂载后回填 tiptap Editor 实例(命令驱动测试用) */ + editorRef?: MutableRefObject; } const MAX_INPUT_HEIGHT = 150; @@ -64,14 +70,19 @@ export function Composer({ busy: busyProp, sendMessage: send, onCommand, + editorRef, }: ComposerProps) { const { t } = useI18n(); - const [text, setText] = useState(""); const [menu, setMenu] = useState(CMD_MENU_CLOSED); + const [hasText, setHasText] = useState(false); const [internalBusy, setInternalBusy] = useState(false); const busy = busyProp ?? internalBusy; const queuedRef = useRef>>(new Map()); - const taRef = useRef(null); + // tiptap 选项闭包只创建一次 → 变化值走 ref 桥接(menu/submit/selectCmd/t) + const menuRef = useRef(menu); + menuRef.current = menu; + const tRef = useRef(t); + tRef.current = t; // 发送函数解析(默认走 preload 桥) const sendFn = @@ -81,21 +92,87 @@ export function Composer({ Promise.reject(new Error("window.emrg.sendMessage unavailable")); }); - // textarea 自适应高度(vanilla:auto → min(scrollHeight,150)) + const editor = useEditor({ + extensions: [ + StarterKit.configure({ link: { openOnClick: false } }), + Placeholder.configure({ placeholder: tRef.current("composer.placeholder") }), + Markdown, + ], + content: "", + editorProps: { + attributes: { + class: "tiptap-input", + "data-testid": "composer-input", + "aria-label": tRef.current("composer.placeholder"), + }, + handleKeyDown: (_view, event) => { + // / 补全菜单键盘导航(vanilla rant 19:44 P1):↑↓ 移动、Enter 选择、Esc 关闭 + const m = menuRef.current; + if (m.items.length > 0) { + if (event.key === "ArrowDown") { + event.preventDefault(); + setMenu(menuNavigate(m, 1)); + return true; + } + if (event.key === "ArrowUp") { + event.preventDefault(); + setMenu(menuNavigate(m, -1)); + return true; + } + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + const item = m.items[m.index]; + if (item) selectCmdRef.current(item.cmd); + return true; + } + if (event.key === "Escape") { + event.preventDefault(); + setMenu(CMD_MENU_CLOSED); + return true; + } + } + // Enter(非 Shift)与 Ctrl+Enter 同发送(Ctrl+Enter 的 ctrlKey 不影响首分支); + // Shift+Enter 交给 tiptap hardBreak(换行) + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + void submitRef.current(); + return true; + } + return false; + }, + }, + onUpdate: ({ editor }) => { + const text = editor.getText(); + const trimmed = text.trim(); + setHasText(trimmed.length > 0); + // / 指令补全:以 / 开头且无空格(仍处于指令词)→ 弹出菜单 + if (trimmed.startsWith("/") && !trimmed.includes(" ")) setMenu(menuForPrefix(trimmed, tRef.current)); + else setMenu(CMD_MENU_CLOSED); + }, + onCreate: ({ editor }) => { + setHasText(!editor.isEmpty); + }, + immediatelyRender: false, + }); + + // 测试注入:editor 实例回填 useEffect(() => { - const ta = taRef.current; - if (!ta) return; - ta.style.height = "auto"; - ta.style.height = `${Math.min(ta.scrollHeight, MAX_INPUT_HEIGHT)}px`; - }, [text]); + if (editorRef) editorRef.current = editor; + return () => { + if (editorRef) editorRef.current = null; + }; + }, [editor, editorRef]); async function submit(): Promise { - const value = text.trim(); + if (!editor) return; + // tiptap-markdown 的 storage 类型不并入 Editor.storage(Storage 泛型)——运行时存在,显式取值 + const md = (editor.storage as unknown as { markdown: { getMarkdown(): string } }).markdown.getMarkdown(); + const value = md.trim(); if (!value) return; const parsed = parseInput(value); if (parsed.type !== "message") { // 指令:清输入 + 关菜单 + 路由(vanilla rant 19:44 P1:/ 开头不进 sendMessage) - setText(""); + editor.commands.clearContent(); setMenu(CMD_MENU_CLOSED); onCommand?.({ type: parsed.type, @@ -113,8 +190,8 @@ export function Composer({ const wasBusy = busy; setInternalBusy(true); store.addUserMessage(value, sid); - setText(""); - // B3:消息已发送 → 清除该会话草稿(React 版草稿由 text state 承载,发送即清) + editor.commands.clearContent(); + // B3:消息已发送 → 清除草稿(tiptap 内容即草稿,发送即清) // G143:send 前预生成 requestId 并标记自有流——消除 IPC 往返竞态窗口 const requestId = genRequestId(); store.setOwnStream(requestId); @@ -127,61 +204,23 @@ export function Composer({ } catch { setInternalBusy(false); store.setOwnStream(null); - // G49:失败恢复输入框,文案不责怪用户(copy.sendFailed) + // G49:失败恢复输入框,文案不责怪用户(copy.sendFailed);markdown 回填 store.addSystemMessage(t("copy.sendFailed"), sid); - setText(value); + editor.commands.setContent(value); } } + const submitRef = useRef<() => Promise>(submit); + submitRef.current = submit; /** 选择补全项:填充输入框 + 关菜单(用户可继续回车执行,vanilla selectCmd) */ function selectCmd(cmd: string): void { - setText(cmd); + if (!editor) return; + editor.commands.setContent(cmd); // tiptap-markdown setContent 解析 markdown → 纯文本段落 setMenu(CMD_MENU_CLOSED); - taRef.current?.focus(); - } - - function onChange(v: string): void { - setText(v); - // / 指令补全:以 / 开头且无空格(仍处于指令词)→ 弹出菜单 - const trimmed = v.trim(); - if (trimmed.startsWith("/") && !trimmed.includes(" ")) setMenu(menuForPrefix(trimmed, t)); - else setMenu(CMD_MENU_CLOSED); - } - - function onKeyDown(e: KeyboardEvent): void { - // / 补全菜单键盘导航(rant 19:44 P1):↑↓ 移动、Enter 选择、Esc 关闭 - if (menu.items.length > 0) { - if (e.key === "ArrowDown") { - e.preventDefault(); - setMenu(menuNavigate(menu, 1)); - return; - } - if (e.key === "ArrowUp") { - e.preventDefault(); - setMenu(menuNavigate(menu, -1)); - return; - } - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - const item = menu.items[menu.index]; - if (item) selectCmd(item.cmd); - return; - } - if (e.key === "Escape") { - e.preventDefault(); - setMenu(CMD_MENU_CLOSED); - return; - } - } - // Enter(非 Shift)与 Ctrl+Enter 同发送 - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - void submit(); - } else if (e.key === "Enter" && e.ctrlKey) { - e.preventDefault(); - void submit(); - } + editor.commands.focus(); } + const selectCmdRef = useRef<(cmd: string) => void>(selectCmd); + selectCmdRef.current = selectCmd; return (
@@ -206,21 +245,11 @@ export function Composer({
) : null}
-