From 9578db10cae2d4a735838aef1f5c4641443bc064 Mon Sep 17 00:00:00 2001
From: YiJie
Date: Thu, 10 Sep 2026 01:04:45 +0800
Subject: [PATCH 1/2] Use Host notifications for plugin operation feedback
---
.agents/docs/notifications.md | 31 +++++
.gitignore | 2 +
AGENTS.md | 16 +++
README.md | 4 +
notification-sdk-evidence.json | 50 ++++++++
package-lock.json | 157 +++++-------------------
package.json | 7 +-
src/chatroom-composer.tsx | 15 +--
src/chatroom-entity-settings.tsx | 16 ++-
src/chatroom-member-details.tsx | 11 +-
src/chatroom-page-loader.tsx | 12 +-
src/chatroom-page.tsx | 22 ++--
src/chatroom-room-actions.tsx | 13 +-
src/chatroom-room-settings.tsx | 10 +-
src/chatroom-timeline-entries.tsx | 13 +-
src/chatroom-timeline.tsx | 24 ++--
src/chatroom.ts | 2 +
src/entity-project-binding.tsx | 23 ++--
src/notifications.tsx | 9 ++
src/team-architecture-navigation.ts | 3 +
src/team-architecture-page.tsx | 7 +-
test/chatroom-composer.mjs | 11 +-
test/chatroom-page-behavior.mjs | 10 +-
test/entity-project-binding.mjs | 2 +-
test/entity-templates.mjs | 4 +-
test/helpers/component-hook-harness.mjs | 12 ++
test/real-cli-room.mjs | 16 ++-
27 files changed, 293 insertions(+), 209 deletions(-)
create mode 100644 .agents/docs/notifications.md
create mode 100644 notification-sdk-evidence.json
create mode 100644 src/notifications.tsx
diff --git a/.agents/docs/notifications.md b/.agents/docs/notifications.md
new file mode 100644
index 0000000..3e41a3e
--- /dev/null
+++ b/.agents/docs/notifications.md
@@ -0,0 +1,31 @@
+# Operation notifications
+
+Room actions, timeline copy/actions, send results, settings and project binding use the Host `ctx.notifications.show()` service. Source icon/name,
+card interactions, queue, expiry and suppression rules belong to Host.
+Declare `notifications` in injection requirements. Field validation and persistent
+business state stay beside the relevant control or object. Do not introduce a
+custom Toast, page-wide operation error, or raw exception message.
+
+The [Host guide](https://github.com/cordisx/cordisx/blob/3158c0401a3f604727f623f1a8ac584017dd8540/.agents/docs/notifications.md)
+owns usage and interaction guidance; the
+[Protocol contract](https://github.com/cordisx/cordisx-protocol/blob/dfa2c5fa956184df11a97955b8e5c74a76cb8876/.agents/docs/notifications-v1.md)
+owns the public API. This migration is an experimental candidate, not a release.
+
+## Reproduce the candidate SDK
+
+The local SDK archive is deliberately excluded from Git. Its exact Host commit,
+Protocol source and hashes are recorded in [notification-sdk-evidence.json](../../notification-sdk-evidence.json).
+From a Host checkout at `efbff656d84b482d51598bc5ba303d24134e0c62`, run the
+[SDK builder](https://github.com/cordisx/cordisx/blob/main/.agents/docs/sdk-source-packaging.md)
+into a fresh directory, then run from this package:
+
+```sh
+mkdir -p .cache/sdk
+cp /path/to/sdk/packages/cordisx-0.1.0-beta.2.tgz .cache/sdk/cordisx-efbff656d84b.tgz
+npm ci --ignore-scripts
+npm run check
+```
+
+Compare the archive SHA-256 with the evidence before installation. Use one shared
+Protocol override; independent copies break branded public types. This explicit
+SDK build avoids recursive Git prepare scripts and does not change the API contract.
diff --git a/.gitignore b/.gitignore
index d2e8361..c8094fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ dist/
coverage/
.DS_Store
cordisx.config.local.json
+
+.cache/sdk/
diff --git a/AGENTS.md b/AGENTS.md
index 66e62dd..dd87906 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -13,3 +13,19 @@
Do not integrate through private Host/native DOM or a standalone replacement page.
- Read `.agents/rules/README.md` before changing this repository.
- Read the organization [CSS ownership and maintenance rule](https://github.com/cordisx/cordisxmono/blob/main/.agents/rules/css.md) before changing CSS, stylesheet-generating code, or a style-bearing DOM contract.
+
+## Operation notifications
+
+Use the public `ctx.notifications.show()` service for operation feedback and
+require `notifications` in plugin injection. Do not create a custom Toast,
+manually positioned alert, or page-wide success/error paragraph. Keep field
+validation and durable business state beside the relevant object. Use stable
+semantic `kind` values, localized safe text, and notification rules owned by Host;
+never expose raw exceptions or notify on every polling attempt.
+See the [Host notification guide](https://github.com/cordisx/cordisx/blob/3158c0401a3f604727f623f1a8ac584017dd8540/.agents/docs/notifications.md)
+for the interaction contract and older-Host capability boundary.
+
+The notification migration currently uses exact feature-branch SDK/Protocol
+revisions; it is not evidence of a formal release or a Mono pointer upgrade.
+
+Candidate setup: [notification migration](./.agents/docs/notifications.md).
diff --git a/README.md b/README.md
index 8696efc..a7b83af 100644
--- a/README.md
+++ b/README.md
@@ -218,3 +218,7 @@ validation; that does not make them a merged or released dependency.
## License
[MIT](LICENSE)
+
+## Notification feedback
+
+See [operation notifications and candidate SDK setup](./.agents/docs/notifications.md).
diff --git a/notification-sdk-evidence.json b/notification-sdk-evidence.json
new file mode 100644
index 0000000..4bbcb09
--- /dev/null
+++ b/notification-sdk-evidence.json
@@ -0,0 +1,50 @@
+{
+ "hostCommit": "efbff656d84b482d51598bc5ba303d24134e0c62",
+ "node": "v24.14.1",
+ "npm": "11.11.0",
+ "sources": [
+ {
+ "location": "node_modules/@cordisx/channel",
+ "spec": "git+ssh://git@github.com/cordisx/plugin-channel.git#4cee12e3a92eeed557bc9de8cc4792710918327a"
+ },
+ {
+ "location": "node_modules/@cordisx/eslint-config",
+ "spec": "git+ssh://git@github.com/cordisx/cordisxmono.git#c63c2e8c2ba7e11502934a52ad2ce3734e804cdc"
+ },
+ {
+ "location": "node_modules/@cordisx/plugin-cli-proxy-api",
+ "spec": "git+ssh://git@github.com/cordisx/plugin-cli-proxy-api.git#1428ee205aab31df2779398cc8491879b303d1d0"
+ },
+ {
+ "location": "node_modules/@cordisx/protocol",
+ "spec": "git+ssh://git@github.com/cordisx/cordisx-protocol.git#f3e18c925c34d90fdf203c158811cef10f7ebf57"
+ }
+ ],
+ "packages": [
+ {
+ "filename": "cordisx-0.1.0-beta.2.tgz",
+ "sha256": "c3ea96b691a8e2e6b52ced3cf1a54fa29830e8b29538112312e036e5d3b5b42a",
+ "integrity": "sha512-f6wHDDz/2+71POc5WEaoiN7xsbHZJMpHob0pxnlH8YCpPAvU2uC8elNHOg7iasuwgry9QrAL5+hEOMRKc0nAgQ=="
+ },
+ {
+ "filename": "cordisx-channel-0.1.0.tgz",
+ "sha256": "ca3fa6283bd5a6af41faf60d936373412246beba047dd652924f5b92060f09e5",
+ "integrity": "sha512-WqKvGxy72WwAUdwoLIHTBCG5/MAwVLKh2VmGK3tdSB+EMSUmuR+lAhxfdt9dYQRQL1Mh3t/w8SdYqps4mO0FkQ=="
+ },
+ {
+ "filename": "cordisx-plugin-cli-proxy-api-0.1.0.tgz",
+ "sha256": "31d23ed0aa49372f2a8b77fcf74e8e80ba0cc8abf19aed8dc06305246f9d7830",
+ "integrity": "sha512-Otc61B3LTLweo0strMzH3XQH5I/P+HXAuFEFN+ynnnfvwscMj5BHovqAz2YS+JvPO79p+3WuCBTnEKh8DN/rkw=="
+ },
+ {
+ "filename": "cordisx-protocol-0.1.0-alpha.0.tgz",
+ "sha256": "04e72b3c58ccb6722a6c56459da1e70c11d0162760eda29bb4846233ea8bb8fe",
+ "integrity": "sha512-o/YzvFs5gkrYsK0vijubLN/go2/oL04sixPz/Ks7N0eMLEHUL7qxD56Yf1ltW5dN9qfBIpgkHuiXgkSgOjj1bw=="
+ },
+ {
+ "filename": "create-cordisx-plugin-0.1.0-beta.2.tgz",
+ "sha256": "3e4d16e315d4a911aabeff3cf6e3b21d64d3d4be7d9b9b07de610443027a5d30",
+ "integrity": "sha512-mdchmWwhZj/C938TpkiaNFjMhTFGzdYeG0bSk5AEhe+NdaoRmP3g0eUhu3DAfsvNNilp+vBbDmT4kWwcH9LwDg=="
+ }
+ ]
+}
diff --git a/package-lock.json b/package-lock.json
index b3b5203..f769784 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,7 @@
},
"devDependencies": {
"@cordisx/eslint-config": "github:cordisx/cordisxmono#c63c2e8c2ba7e11502934a52ad2ce3734e804cdc",
- "@cordisx/protocol": "github:cordisx/cordisx-protocol#a39c5d1f61999a69043180d6b7e7bbbee3072c09",
+ "@cordisx/protocol": "github:cordisx/cordisx-protocol#f3e18c925c34d90fdf203c158811cef10f7ebf57",
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/schemastery": "3.18.1",
"@oneworks/avatar": "1.0.0-rc.9",
@@ -25,7 +25,7 @@
"@vitejs/plugin-react": "6.1.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
- "cordisx": "github:cordisx/cordisx#e0a7d7b6fbd653dcf55b4105d3d193e49443826f",
+ "cordisx": "file:.cache/sdk/cordisx-efbff656d84b.tgz",
"dprint": "0.57.1",
"esbuild": "^0.28.2",
"eslint": "9.39.4",
@@ -143,16 +143,6 @@
"@keyv/serialize": "^1.1.1"
}
},
- "node_modules/@cordisx/channel": {
- "version": "0.1.0",
- "resolved": "git+ssh://git@github.com/cordisx/plugin-channel.git#4cee12e3a92eeed557bc9de8cc4792710918327a",
- "integrity": "sha512-WqKvGxy72WwAUdwoLIHTBCG5/MAwVLKh2VmGK3tdSB+EMSUmuR+lAhxfdt9dYQRQL1Mh3t/w8SdYqps4mO0FkQ==",
- "dev": true,
- "license": "AGPL-3.0-or-later",
- "engines": {
- "node": ">=22.19"
- }
- },
"node_modules/@cordisx/eslint-config": {
"version": "0.1.0",
"resolved": "git+ssh://git@github.com/cordisx/cordisxmono.git#c63c2e8c2ba7e11502934a52ad2ce3734e804cdc",
@@ -165,20 +155,10 @@
"eslint": "^9.39.4 || ^10.0.0"
}
},
- "node_modules/@cordisx/plugin-cli-proxy-api": {
- "version": "0.1.0",
- "resolved": "git+ssh://git@github.com/cordisx/plugin-cli-proxy-api.git#1428ee205aab31df2779398cc8491879b303d1d0",
- "integrity": "sha512-EyIa/JSSHhbd9buCUd2gnI/Cdo47GYmJC+3FelfTNrA/krZSJN8TFE1c09yhySUtImtUliGv7ICa0XG+Ix/Kww==",
- "dev": true,
- "license": "AGPL-3.0-or-later",
- "engines": {
- "node": ">=22.19"
- }
- },
"node_modules/@cordisx/protocol": {
"version": "0.1.0-alpha.0",
- "resolved": "git+ssh://git@github.com/cordisx/cordisx-protocol.git#a39c5d1f61999a69043180d6b7e7bbbee3072c09",
- "integrity": "sha512-42+ErrJ8cJvXj0LB9Qt3+kAycATyXT5ifsM3HfjtwYDtSBTcSVZmhF/Tod5fFzut2jM7IvWvyr5SF1jxXKpnLQ==",
+ "resolved": "git+ssh://git@github.com/cordisx/cordisx-protocol.git#f3e18c925c34d90fdf203c158811cef10f7ebf57",
+ "integrity": "sha512-o/YzvFs5gkrYsK0vijubLN/go2/oL04sixPz/Ks7N0eMLEHUL7qxD56Yf1ltW5dN9qfBIpgkHuiXgkSgOjj1bw==",
"dev": true,
"license": "AGPL-3.0-or-later",
"engines": {
@@ -2796,22 +2776,20 @@
}
},
"node_modules/cordisx": {
- "name": "cordisx-monorepo",
"version": "0.1.0-beta.2",
- "resolved": "git+ssh://git@github.com/cordisx/cordisx.git#e0a7d7b6fbd653dcf55b4105d3d193e49443826f",
- "integrity": "sha512-YTPu42VaCvABOAmgstRlBBmsn8YzNDw5KRa4PtfVXjWWu3mr3qUxVVEoBDTFr+qgh+PrHz74SsHSlCj7Hy5pnQ==",
+ "resolved": "file:.cache/sdk/cordisx-efbff656d84b.tgz",
+ "integrity": "sha512-f6wHDDz/2+71POc5WEaoiN7xsbHZJMpHob0pxnlH8YCpPAvU2uC8elNHOg7iasuwgry9QrAL5+hEOMRKc0nAgQ==",
"bundleDependencies": [
- "@cordisx/schemastery-ui"
+ "@cordisx/schemastery-ui",
+ "@cordisx/channel",
+ "@cordisx/plugin-cli-proxy-api"
],
"dev": true,
"license": "AGPL-3.0-or-later",
- "workspaces": [
- "packages/*"
- ],
"dependencies": {
- "@cordisx/channel": "github:cordisx/plugin-channel#4cee12e3a92eeed557bc9de8cc4792710918327a",
- "@cordisx/plugin-cli-proxy-api": "github:cordisx/plugin-cli-proxy-api#1428ee205aab31df2779398cc8491879b303d1d0",
- "@cordisx/protocol": "github:cordisx/cordisx-protocol#a39c5d1f61999a69043180d6b7e7bbbee3072c09",
+ "@cordisx/channel": "0.1.0",
+ "@cordisx/plugin-cli-proxy-api": "0.1.0",
+ "@cordisx/protocol": "github:cordisx/cordisx-protocol#f3e18c925c34d90fdf203c158811cef10f7ebf57",
"@cordisx/schemastery-ui": "0.1.0-beta.2",
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/schemastery": "^3.18.1",
@@ -2821,6 +2799,7 @@
"@shikitor/core": "1.0.2",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.5",
+ "@vitejs/plugin-react": "6.1.0",
"debug": "4.4.3",
"es-module-lexer": "1.7.0",
"esbuild": "0.28.2",
@@ -2843,15 +2822,34 @@
"swr": "2.5.1",
"tar": "7.5.22",
"tdesign-react": "1.18.2",
+ "vite": "8.2.2",
"ws": "^8.18.3"
},
"bin": {
- "cordisx": "packages/cli/dist/src/cli.js"
+ "cordisx": "dist/src/cli.js"
},
"engines": {
"node": ">=22.19"
}
},
+ "node_modules/cordisx/node_modules/@cordisx/channel": {
+ "version": "0.1.0",
+ "dev": true,
+ "inBundle": true,
+ "license": "AGPL-3.0-or-later",
+ "engines": {
+ "node": ">=22.19"
+ }
+ },
+ "node_modules/cordisx/node_modules/@cordisx/plugin-cli-proxy-api": {
+ "version": "0.1.0",
+ "dev": true,
+ "inBundle": true,
+ "license": "AGPL-3.0-or-later",
+ "engines": {
+ "node": ">=22.19"
+ }
+ },
"node_modules/cordisx/node_modules/@cordisx/schemastery-ui": {
"version": "0.1.0-beta.2",
"dev": true,
@@ -2878,74 +2876,6 @@
"react-dom": ">=18.3.0 <20"
}
},
- "node_modules/cordisx/packages/cli": {
- "name": "cordisx",
- "version": "0.1.0-beta.2",
- "bundleDependencies": [
- "@cordisx/schemastery-ui"
- ],
- "extraneous": true,
- "license": "AGPL-3.0-or-later",
- "dependencies": {
- "@cordisx/channel": "github:cordisx/plugin-channel#4cee12e3a92eeed557bc9de8cc4792710918327a",
- "@cordisx/plugin-cli-proxy-api": "github:cordisx/plugin-cli-proxy-api#1428ee205aab31df2779398cc8491879b303d1d0",
- "@cordisx/protocol": "github:cordisx/cordisx-protocol#a39c5d1f61999a69043180d6b7e7bbbee3072c09",
- "@cordisx/schemastery-ui": "0.1.0-beta.2",
- "@deepseek-ai/cordis": "4.0.1",
- "@deepseek-ai/schemastery": "^3.18.1",
- "@larksuiteoapi/node-sdk": "^1.73.0",
- "@oneworks/avatar": "1.0.0-rc.8",
- "@oneworks/avatar-react": "1.0.0-rc.8",
- "@shikitor/core": "1.0.2",
- "@types/react": "19.2.18",
- "@types/react-dom": "19.2.5",
- "@vitejs/plugin-react": "6.1.0",
- "debug": "4.4.3",
- "es-module-lexer": "1.7.0",
- "esbuild": "0.28.2",
- "extend": "3.0.2",
- "intl-messageformat": "11.2.14",
- "lightningcss": "1.33.0",
- "luna-console": "1.3.6",
- "luna-data-grid": "1.6.5",
- "luna-dom-viewer": "1.8.4",
- "luna-object-viewer": "0.3.2",
- "react": "19.2.8",
- "react-dom": "19.2.8",
- "react-markdown": "10.1.0",
- "rehype-raw": "^7.0.0",
- "rehype-sanitize": "^6.0.0",
- "reicon": "1.2.1",
- "remark-gfm": "4.0.1",
- "shiki": "4.4.3",
- "style-to-js": "1.1.21",
- "swr": "2.5.1",
- "tar": "7.5.22",
- "tdesign-react": "1.18.2",
- "vite": "8.2.2",
- "ws": "^8.18.3"
- },
- "bin": {
- "cordisx": "dist/src/cli.js"
- },
- "devDependencies": {
- "@cordisx/channel-runtime": "file:../channel-runtime",
- "@cordisx/protocol": "github:cordisx/cordisx-protocol#a39c5d1f61999a69043180d6b7e7bbbee3072c09",
- "@types/node": "^22.18.1",
- "@types/react": "19.2.18",
- "@types/react-dom": "19.2.5",
- "@types/ws": "^8.18.1",
- "ajv": "8.20.0",
- "ajv-formats": "3.0.1",
- "jsdom": "^26.1.0",
- "tsx": "^4.20.5",
- "typescript": "^5.9.2",
- "vitest": "^3.2.4"
- },
- "engines": {
- "node": ">=22.19"
- }
- },
"node_modules/cosmiconfig": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
@@ -7783,27 +7713,6 @@
"dev": true,
"license": "0BSD"
},
- "node_modules/tsx": {
- "version": "4.23.13",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz",
- "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "esbuild": "~0.28.0"
- },
- "bin": {
- "tsx": "dist/cli.mjs"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- }
- },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
diff --git a/package.json b/package.json
index 3ef0fd3..a7b338a 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
},
"devDependencies": {
"@cordisx/eslint-config": "github:cordisx/cordisxmono#c63c2e8c2ba7e11502934a52ad2ce3734e804cdc",
- "@cordisx/protocol": "github:cordisx/cordisx-protocol#a39c5d1f61999a69043180d6b7e7bbbee3072c09",
+ "@cordisx/protocol": "github:cordisx/cordisx-protocol#f3e18c925c34d90fdf203c158811cef10f7ebf57",
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/schemastery": "3.18.1",
"@oneworks/avatar": "1.0.0-rc.9",
@@ -47,7 +47,7 @@
"@vitejs/plugin-react": "6.1.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
- "cordisx": "github:cordisx/cordisx#e0a7d7b6fbd653dcf55b4105d3d193e49443826f",
+ "cordisx": "file:.cache/sdk/cordisx-efbff656d84b.tgz",
"dprint": "0.57.1",
"esbuild": "^0.28.2",
"eslint": "9.39.4",
@@ -64,5 +64,8 @@
},
"bin": {
"cordisx-chatroom": "./dist/agent-tools/cli/chatroom.mjs"
+ },
+ "overrides": {
+ "@cordisx/protocol": "$@cordisx/protocol"
}
}
diff --git a/src/chatroom-composer.tsx b/src/chatroom-composer.tsx
index 6cee7c2..c1e32d8 100644
--- a/src/chatroom-composer.tsx
+++ b/src/chatroom-composer.tsx
@@ -1,3 +1,4 @@
+import { useNotifications } from './notifications.js';
import { type KeyboardEvent, useEffect, useId, useLayoutEffect, useRef, useState } from 'cordisx/react';
import type { CordisXReactPageProps } from 'cordisx/contracts';
import {
@@ -117,7 +118,7 @@ export function ChatroomComposer(
const [draft, setDraft] = useState('');
const [sending, setSending] = useState(false);
const [aborted, setAborted] = useState(signal.aborted);
- const [notice, setNotice] = useState();
+ const notifications = useNotifications();
const [error, setError] = useState();
const [query, setQuery] = useState();
const [activeIndex, setActiveIndex] = useState(0);
@@ -231,7 +232,6 @@ export function ChatroomComposer(
setSending(true);
setQuery(undefined);
setError(undefined);
- setNotice(undefined);
try {
if (firstMessage !== undefined) {
const result = await firstMessage(submittedDraft);
@@ -252,14 +252,16 @@ export function ChatroomComposer(
if (!mounted.current || signal.aborted) return;
const result = source.pageComposerCompletion(completion);
if (result.status !== 'accepted') {
- setError(t('composer.send-failed'));
+ notifications.show({ kind: 'message.send-failed', type: 'error', message: t('composer.send-failed') });
return;
}
}
setDraft(current => current === submittedDraft ? '' : current);
- setNotice(t('composer.sent'));
+ notifications.show({ kind: 'message.sent', type: 'success', message: t('composer.sent') });
} catch {
- if (mounted.current && !signal.aborted) setError(t('composer.send-failed'));
+ if (mounted.current && !signal.aborted) {
+ notifications.show({ kind: 'message.send-failed', type: 'error', message: t('composer.send-failed') });
+ }
} finally {
busy.current = false;
if (mounted.current && !signal.aborted) setSending(false);
@@ -355,7 +357,6 @@ export function ChatroomComposer(
onValueChange={value => {
draftRef.current = value;
setDraft(value);
- setNotice(undefined);
setError(undefined);
updateQuery(value, input.current?.getSelection());
}}
@@ -414,7 +415,7 @@ export function ChatroomComposer(
{t(shortcutPolicy === 'enter' ? 'composer.shortcut.enter' : 'composer.shortcut.mod-enter')}
- {unavailable ? t('composer.unavailable') : sending ? t('composer.sending') : notice}
+ {unavailable ? t('composer.unavailable') : sending ? t('composer.sending') : undefined}
{error !== undefined && {error}
}
diff --git a/src/chatroom-entity-settings.tsx b/src/chatroom-entity-settings.tsx
index 53f2599..7916c17 100644
--- a/src/chatroom-entity-settings.tsx
+++ b/src/chatroom-entity-settings.tsx
@@ -1,3 +1,4 @@
+import { useNotifications } from './notifications.js';
import { useEffect, useRef, useState } from 'cordisx/react';
import { Button, Icon } from 'cordisx/ui';
import type { CordisXReactPageProps } from 'cordisx/contracts';
@@ -12,13 +13,12 @@ export function ChatroomEntitySettings({ room, participantId, details, t }: {
}) {
const [available, setAvailable] = useState(false);
const [busy, setBusy] = useState(false);
- const [failed, setFailed] = useState(false);
+ const notifications = useNotifications();
const pending = useRef(false);
const member = room.memberships.find(candidate => candidate.participantId === participantId);
useEffect(() => {
let cancelled = false;
setAvailable(false);
- setFailed(false);
void details.entitySettingsAvailable(room, participantId).then(value => {
if (!cancelled) setAvailable(value);
}).catch(() => {});
@@ -37,11 +37,16 @@ export function ChatroomEntitySettings({ room, participantId, details, t }: {
if (pending.current) return;
pending.current = true;
setBusy(true);
- setFailed(false);
try {
- if (!await details.openEntitySettings(room, participantId)) setFailed(true);
+ if (!await details.openEntitySettings(room, participantId)) {
+ notifications.show({
+ kind: 'settings.open-failed',
+ type: 'error',
+ message: t('identity.settings.failed'),
+ });
+ }
} catch {
- setFailed(true);
+ notifications.show({ kind: 'settings.open-failed', type: 'error', message: t('identity.settings.failed') });
} finally {
pending.current = false;
setBusy(false);
@@ -50,7 +55,6 @@ export function ChatroomEntitySettings({ room, participantId, details, t }: {
>
- {failed && {t('identity.settings.failed')}
}
);
}
diff --git a/src/chatroom-member-details.tsx b/src/chatroom-member-details.tsx
index b424add..38273c9 100644
--- a/src/chatroom-member-details.tsx
+++ b/src/chatroom-member-details.tsx
@@ -1,3 +1,4 @@
+import { useNotifications } from './notifications.js';
import { ChatroomEntitySettings } from './chatroom-entity-settings.js';
import { useEffect, useState } from 'cordisx/react';
import { Button, MarkdownViewer } from 'cordisx/ui';
@@ -20,7 +21,7 @@ export function ChatroomMemberDetails({ snapshot, participantId, details, t }: {
const [entity, setEntity] = useState();
const [loading, setLoading] = useState(true);
const [opening, setOpening] = useState();
- const [error, setError] = useState(false);
+ const notifications = useNotifications();
const room = snapshot.room;
const member = room?.memberships.find(candidate => candidate.participantId === participantId);
useEffect(() => {
@@ -54,11 +55,12 @@ export function ChatroomMemberDetails({ snapshot, participantId, details, t }: {
const open = async (sessionId: typeof sessions[number]['sessionId']) => {
if (opening !== undefined) return;
setOpening(sessionId);
- setError(false);
try {
- if (!await details.openSession(room, participantId, sessionId)) setError(true);
+ if (!await details.openSession(room, participantId, sessionId)) {
+ notifications.show({ kind: 'session.open-failed', type: 'error', message: t('identity.session.open-failed') });
+ }
} catch {
- setError(true);
+ notifications.show({ kind: 'session.open-failed', type: 'error', message: t('identity.session.open-failed') });
} finally {
setOpening(undefined);
}
@@ -102,7 +104,6 @@ export function ChatroomMemberDetails({ snapshot, participantId, details, t }: {
))}
)}
- {error && {t('identity.session.open-failed')}
}
{unassignedTasks.length > 0 && (
diff --git a/src/chatroom-page-loader.tsx b/src/chatroom-page-loader.tsx
index da97cf5..354d8d9 100644
--- a/src/chatroom-page-loader.tsx
+++ b/src/chatroom-page-loader.tsx
@@ -1,5 +1,6 @@
import { type ComponentType, defineReactPage, lazy, Suspense } from 'cordisx/react';
-import type { CordisXReactPageProps } from 'cordisx/contracts';
+import { NotificationsContext } from './notifications.js';
+import type { CordisXReactPageProps, NotificationsV1 } from 'cordisx/contracts';
import type { ChatroomPageSource } from './chatroom-page-source.js';
import type { ChatroomPageDetails } from './chatroom-page-details.js';
@@ -25,6 +26,7 @@ export const createChatroomPageModuleLoader = (
/** Registration stays light; the page graph starts loading only on an actual mount. */
export function createLazyChatroomPage(
source: ChatroomPageSource,
+ notifications: NotificationsV1,
imageCache: ChatroomSidebarImageCache,
details?: ChatroomPageDetails,
) {
@@ -35,8 +37,10 @@ export function createLazyChatroomPage(
default: (await loadChatroomPageModule()).ChatroomPage,
}));
return defineReactPage(props => (
-
-
-
+
+
+
+
+
));
}
diff --git a/src/chatroom-page.tsx b/src/chatroom-page.tsx
index 6971b42..979aeb7 100644
--- a/src/chatroom-page.tsx
+++ b/src/chatroom-page.tsx
@@ -1,3 +1,4 @@
+import { useNotifications } from './notifications.js';
import { ChatroomLeaderPicker } from './chatroom-new-room.js';
import {
type CSSProperties,
@@ -60,13 +61,13 @@ export function ChatroomPage(
) {
const [inspector, setInspector] = useState();
const [selectedLeader, setSelectedLeader] = useState();
- const [roomOpenFailed, setRoomOpenFailed] = useState(false);
+
const root = useRef(null);
const panel = useRef(null);
const { width, narrow, separatorProps } = useChatroomInspector(root, inspector !== undefined, props.signal);
const [memberSearch, setMemberSearch] = useState('');
const [mentionRequest, setMentionRequest] = useState<{ participantId: string; sequence: number; }>();
- const [actionError, setActionError] = useState(false);
+ const notifications = useNotifications();
const [busyAction, setBusyAction] = useState();
const actionPending = useRef(false);
const actionGeneration = useRef(0);
@@ -95,12 +96,13 @@ export function ChatroomPage(
if (action.disabled || actionPending.current || props.signal.aborted) return;
actionPending.current = true;
setBusyAction(action.id);
- setActionError(false);
const generation = actionGeneration.current;
try {
await action.run();
} catch {
- if (generation === actionGeneration.current && !props.signal.aborted) setActionError(true);
+ if (generation === actionGeneration.current && !props.signal.aborted) {
+ notifications.show({ kind: 'room.action-failed', type: 'error', message: props.t('page.action.failed') });
+ }
} finally {
if (generation === actionGeneration.current && !props.signal.aborted) {
actionPending.current = false;
@@ -113,7 +115,6 @@ export function ChatroomPage(
setInspector(undefined);
setMemberSearch('');
setMentionRequest(undefined);
- setActionError(false);
setBusyAction(undefined);
actionPending.current = false;
actionGeneration.current += 1;
@@ -280,7 +281,6 @@ export function ChatroomPage(
- {actionError &&
{props.t('page.action.failed')}
}
{roomId === undefined && details !== undefined
? (
)}
- {roomOpenFailed &&
{props.t('task.start.open-failed')}
}
{
- setRoomOpenFailed(false);
const result = await details.startRoom(text, selectedLeader, props.signal);
if (props.signal.aborted) return { status: 'unavailable', message: props.t('composer.unavailable') };
if (result.status !== 'accepted') {
@@ -336,7 +334,13 @@ export function ChatroomPage(
try {
await props.navigation.navigate({ id: 'room', params: { roomId: result.roomId } });
} catch {
- if (!props.signal.aborted) setRoomOpenFailed(true);
+ if (!props.signal.aborted) {
+ notifications.show({
+ kind: 'room.open-failed',
+ type: 'error',
+ message: props.t('task.start.open-failed'),
+ });
+ }
}
return { status: 'accepted' };
}
diff --git a/src/chatroom-room-actions.tsx b/src/chatroom-room-actions.tsx
index 063cd7e..42f35a4 100644
--- a/src/chatroom-room-actions.tsx
+++ b/src/chatroom-room-actions.tsx
@@ -1,3 +1,4 @@
+import { useNotifications } from './notifications.js';
import { type KeyboardEvent, useEffect, useRef, useState } from 'cordisx/react';
import { Button } from 'cordisx/ui';
import type { CordisXReactPageProps } from 'cordisx/contracts';
@@ -22,7 +23,7 @@ export function ChatroomRoomActions({ room, details, t, onDeleted }: {
const dialog = useRef(null);
const [open, setOpen] = useState(false);
const [confirmation, setConfirmation] = useState();
- const [feedback, setFeedback] = useState<{ text: string; failed: boolean; }>();
+ const notifications = useNotifications();
const [busy, setBusy] = useState(false);
const pending = useRef(false);
const [clipboardAvailable, setClipboardAvailable] = useState(false);
@@ -59,7 +60,6 @@ export function ChatroomRoomActions({ room, details, t, onDeleted }: {
if (pending.current || action.disabled.value) return;
pending.current = true;
setBusy(true);
- setFeedback(undefined);
try {
if (action.kind === 'command') {
await details.executeRoomAction(room.id, action.id);
@@ -70,11 +70,15 @@ export function ChatroomRoomActions({ room, details, t, onDeleted }: {
if (text === undefined) throw new Error('Room link unavailable');
await clipboard.writeText(text);
}
- setFeedback({ text: display(action.feedback.success, t), failed: false });
+ notifications.show({
+ kind: 'room.action-completed',
+ type: 'success',
+ message: display(action.feedback.success, t),
+ });
close();
if (action.id === 'delete') await onDeleted();
} catch {
- setFeedback({ text: display(action.feedback.failure, t), failed: true });
+ notifications.show({ kind: 'room.action-failed', type: 'error', message: display(action.feedback.failure, t) });
} finally {
pending.current = false;
setBusy(false);
@@ -167,7 +171,6 @@ export function ChatroomRoomActions({ room, details, t, onDeleted }: {
)}
- {feedback &&
{feedback.text}
}
);
}
diff --git a/src/chatroom-room-settings.tsx b/src/chatroom-room-settings.tsx
index 9e250fc..9713ac7 100644
--- a/src/chatroom-room-settings.tsx
+++ b/src/chatroom-room-settings.tsx
@@ -1,3 +1,4 @@
+import { useNotifications } from './notifications.js';
import { useRef, useState } from 'cordisx/react';
import { Button } from 'cordisx/ui';
import type { CordisXReactPageProps } from 'cordisx/contracts';
@@ -12,11 +13,12 @@ export function ChatroomRoomSettings({ roomId, details, t, onSaved }: {
readonly t: CordisXReactPageProps['t'];
readonly onSaved?: () => void;
}) {
+ const notifications = useNotifications();
const [original, setOriginal] = useState(() => details.profile(roomId));
const [name, setName] = useState(original?.room.title ?? '');
const [description, setDescription] = useState(original?.room.description ?? '');
const [busy, setBusy] = useState(false);
- const [feedback, setFeedback] = useState<'saved' | 'failed' | 'name-invalid' | 'description-invalid'>();
+ const [feedback, setFeedback] = useState<'name-invalid' | 'description-invalid'>();
const saving = useRef(false);
const dirty = name !== (original?.room.title ?? '') || description !== (original?.room.description ?? '');
const save = async () => {
@@ -35,10 +37,10 @@ export function ChatroomRoomSettings({ roomId, details, t, onSaved }: {
try {
await details.saveProfile(roomId, original.revision, name, description);
setOriginal(details.profile(roomId));
- setFeedback('saved');
+ notifications.show({ kind: 'room.settings', type: 'success', message: t('room.settings.saved') });
onSaved?.();
} catch {
- setFeedback('failed');
+ notifications.show({ kind: 'room.settings', type: 'error', message: t('room.settings.failed') });
} finally {
saving.current = false;
setBusy(false);
@@ -74,7 +76,7 @@ export function ChatroomRoomSettings({ roomId, details, t, onSaved }: {
{feedback === undefined
? null
- : {t(`room.settings.${feedback}`)}
}
+ : {t(`room.settings.${feedback}`)}
}
);
}
diff --git a/src/chatroom-timeline-entries.tsx b/src/chatroom-timeline-entries.tsx
index 436288c..42a3da2 100644
--- a/src/chatroom-timeline-entries.tsx
+++ b/src/chatroom-timeline-entries.tsx
@@ -1,3 +1,4 @@
+import { useNotifications } from './notifications.js';
import { type KeyboardEvent, type MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'cordisx/react';
import { Button, Icon } from 'cordisx/ui';
import type { CordisXReactPageProps } from 'cordisx/contracts';
@@ -380,16 +381,19 @@ export function ApprovalItem(
}
}, [item.state]);
const [busy, setBusy] = useState(false);
- const [error, setError] = useState(false);
+ const notifications = useNotifications();
const decide = async (decision: 'approved' | 'denied') => {
if (pending.current || item.state !== 'pending') return;
pending.current = true;
setBusy(true);
- setError(false);
try {
- if (!await source.decideApproval(roomId, item.itemId, decision) && mounted.current) setError(true);
+ if (!await source.decideApproval(roomId, item.itemId, decision) && mounted.current) {
+ notifications.show({ kind: 'approval.decision-failed', type: 'error', message: t('approval.decision.failed') });
+ }
} catch {
- if (mounted.current) setError(true);
+ if (mounted.current) {
+ notifications.show({ kind: 'approval.decision-failed', type: 'error', message: t('approval.decision.failed') });
+ }
} finally {
pending.current = false;
if (mounted.current) setBusy(false);
@@ -499,7 +503,6 @@ export function ApprovalItem(
)}
)}
- {error && {t('approval.decision.failed')}
}
);
}
diff --git a/src/chatroom-timeline.tsx b/src/chatroom-timeline.tsx
index fdbcf7d..3c2d23f 100644
--- a/src/chatroom-timeline.tsx
+++ b/src/chatroom-timeline.tsx
@@ -1,3 +1,4 @@
+import { useNotifications } from './notifications.js';
import { useEffect, useLayoutEffect, useRef, useState } from 'cordisx/react';
import { EmptyState } from 'cordisx/ui';
import type { CordisXReactPageProps } from 'cordisx/contracts';
@@ -52,8 +53,7 @@ export function ChatroomTimeline(
const region = useRef(null);
const menuElement = useRef(null);
const [actions, setActions] = useState();
- const [copyStatus, setCopyStatus] = useState<'copied' | 'copy-failed'>();
- const [actionFailed, setActionFailed] = useState(false);
+ const notifications = useNotifications();
const [runningActions, setRunningActions] = useState>(() => new Set());
const actionPending = useRef(new Set());
const copying = useRef(false);
@@ -105,12 +105,13 @@ export function ChatroomTimeline(
if (actionPending.current.has(key)) return;
actionPending.current.add(key);
setRunningActions(new Set(actionPending.current));
- setActionFailed(false);
setActions(undefined);
try {
await source.executeMessageAction(roomId, itemId, actionId);
} catch {
- if (mounted.current) setActionFailed(true);
+ if (mounted.current) {
+ notifications.show({ kind: 'timeline.action-failed', type: 'error', message: t('timeline.action-failed') });
+ }
} finally {
actionPending.current.delete(key);
if (mounted.current) setRunningActions(new Set(actionPending.current));
@@ -138,17 +139,20 @@ export function ChatroomTimeline(
const write = copyText
?? (typeof clipboard?.writeText === 'function' ? (value: string) => clipboard.writeText(value) : undefined);
if (write === undefined) {
- setCopyStatus('copy-failed');
+ notifications.show({ kind: 'timeline.copy', type: 'error', message: t('timeline.copy-failed') });
return;
}
copying.current = true;
- setCopyStatus(undefined);
closeActions(true);
try {
await write(text);
- if (mounted.current) setCopyStatus('copied');
+ if (mounted.current) {
+ notifications.show({ kind: 'timeline.copy', type: 'success', message: t('timeline.copied') });
+ }
} catch {
- if (mounted.current) setCopyStatus('copy-failed');
+ if (mounted.current) {
+ notifications.show({ kind: 'timeline.copy', type: 'error', message: t('timeline.copy-failed') });
+ }
} finally {
copying.current = false;
}
@@ -291,10 +295,6 @@ export function ChatroomTimeline(
)}
)}
- {copyStatus !== undefined && (
- {t(`timeline.${copyStatus}`)}
- )}
- {actionFailed && {t('timeline.action-failed')}
}
({
} as const);
export const inject = [
+ 'notifications',
'i18n',
'commands',
'pages',
@@ -481,6 +482,7 @@ export async function apply(ctx: Context, config: unknown = {}): Promise {
const { createLazyChatroomPage } = await import('./chatroom-page-loader.js');
const pageMount = createLazyChatroomPage(
pageSource,
+ ctx.notifications,
product.sidebarImages,
new ChatroomPageDetails({
entitySettings: ctx.entitySettingsNavigation,
diff --git a/src/entity-project-binding.tsx b/src/entity-project-binding.tsx
index 4efd307..5b3e3fe 100644
--- a/src/entity-project-binding.tsx
+++ b/src/entity-project-binding.tsx
@@ -1,3 +1,4 @@
+import { useNotifications } from './notifications.js';
import { useEffect, useRef, useState } from 'cordisx/react';
import { Button } from 'cordisx/ui';
import type { AgentDefinitionIdentity } from '@cordisx/protocol/agents/v1';
@@ -25,6 +26,7 @@ export function EntityProjectBinding({ identity, contexts, t }: {
readonly contexts?: EntityExecutionContexts;
readonly t: Translate;
}) {
+ const notifications = useNotifications();
const [snapshot, setSnapshot] = useState();
const [projects, setProjects] = useState();
const [selected, setSelected] = useState('');
@@ -92,7 +94,7 @@ export function EntityProjectBinding({ identity, contexts, t }: {
const currentEpoch = epoch.current;
const key = JSON.stringify([identity, snapshot.revision, selected]);
if (mutation.current !== undefined && mutation.current.key !== key) {
- setFeedback(t('detail.project-conflict'));
+ notifications.show({ kind: 'project.save', type: 'error', message: t('detail.project-conflict') });
return;
}
mutation.current ??= { key, id: `entity-project.${crypto.randomUUID()}` };
@@ -110,12 +112,20 @@ export function EntityProjectBinding({ identity, contexts, t }: {
if (result.status === 'applied') {
mutation.current = undefined;
setSnapshot(result);
- setFeedback(t('detail.project-saved'));
- } else {setFeedback(
- t(result.status === 'conflict' ? 'detail.project-conflict' : 'detail.project-unavailable'),
- );}
+ notifications.show({ kind: 'project.save', type: 'success', message: t('detail.project-saved') });
+ } else {notifications.show({
+ kind: 'project.save',
+ type: 'error',
+ message: t(result.status === 'conflict' ? 'detail.project-conflict' : 'detail.project-unavailable'),
+ });}
} catch {
- if (mounted.current && epoch.current === currentEpoch) setFeedback(t('detail.project-unavailable'));
+ if (mounted.current && epoch.current === currentEpoch) {
+ notifications.show({
+ kind: 'project.save',
+ type: 'error',
+ message: t('detail.project-unavailable'),
+ });
+ }
} finally {
if (epoch.current === currentEpoch) {
pending.current = false;
@@ -127,7 +137,6 @@ export function EntityProjectBinding({ identity, contexts, t }: {
{t('detail.project-save')}
{t('detail.project-future-only')}
- {feedback && {feedback}
}
);
}
diff --git a/src/notifications.tsx b/src/notifications.tsx
new file mode 100644
index 0000000..b25c4df
--- /dev/null
+++ b/src/notifications.tsx
@@ -0,0 +1,9 @@
+import { createContext, useContext } from 'cordisx/react';
+import type { NotificationsV1 } from 'cordisx/contracts';
+
+export const NotificationsContext = createContext(undefined);
+export function useNotifications(): NotificationsV1 {
+ const service = useContext(NotificationsContext);
+ if (service === undefined) throw new Error('Chatroom requires the Host notifications service.');
+ return service;
+}
diff --git a/src/team-architecture-navigation.ts b/src/team-architecture-navigation.ts
index c01e10f..af5d5f9 100644
--- a/src/team-architecture-navigation.ts
+++ b/src/team-architecture-navigation.ts
@@ -1,3 +1,4 @@
+import type { NotificationsV1 } from 'cordisx/contracts';
import {
CORDISX_MANAGER_CONTENT_NAVIGATION_SCHEMA_V1,
CORDISX_MANAGER_CONTENT_NAVIGATION_SCHEMA_V2,
@@ -590,6 +591,7 @@ export const TEAM_ARCHITECTURE_LOCALES: readonly CordisXLocaleCatalog context.i18n.define(catalog)));
const pageMount = createTeamArchitecturePage(
source,
+ context.notifications,
TEAM_ARCHITECTURE_DETAIL_ROUTE_IDS,
context.i18n.seat(TEAM_ARCHITECTURE_LOCALE_NAMESPACE),
);
diff --git a/src/team-architecture-page.tsx b/src/team-architecture-page.tsx
index 97a0eed..e72e324 100644
--- a/src/team-architecture-page.tsx
+++ b/src/team-architecture-page.tsx
@@ -1,3 +1,5 @@
+import { NotificationsContext } from './notifications.js';
+import type { NotificationsV1 } from 'cordisx/contracts';
import { EntityProjectBinding } from './entity-project-binding.js';
import {
Fragment,
@@ -909,10 +911,13 @@ function projectEntities(snapshot: ReturnType,
) {
return defineReactPage(props => (
-
+
+
+
));
}
diff --git a/test/chatroom-composer.mjs b/test/chatroom-composer.mjs
index dcf71b2..e4e48d8 100644
--- a/test/chatroom-composer.mjs
+++ b/test/chatroom-composer.mjs
@@ -5,8 +5,11 @@ import { build } from 'esbuild';
// Exercise the production component's handlers and effect cleanup with a bounded
// hook host. This is component behavior evidence, not native renderer acceptance.
const hooks = `
+export const notifications = [];
+export function createContext(value) { return {value}; }
+export function useContext() { return { show: options => { notifications.push(options); return { dismiss() {} }; } }; }
let slots = [], cursor = 0, pending = [];
-export function reset() { slots = []; }
+export function reset() { slots = []; notifications.length = 0; }
export function begin() { cursor = 0; pending = []; }
export function finish() { for (const effect of pending) effect(); }
export function cleanup() { for (const slot of slots) slot?.cleanup?.(); }
@@ -239,7 +242,7 @@ test('only one public command executes while busy; accepted completion clears th
await settle(ui);
assert.equal(ui.input().props.value, '');
assert.equal(ui.input().props.disabled, false);
- assert.equal(ui.find(node => node.props?.role === 'status').props.children, 'composer.sent');
+ assert.equal(module.hooks.notifications.at(-1).message, 'composer.sent');
});
test('failed completion retains the draft and retry uses the same public seam', async () => {
@@ -248,7 +251,7 @@ test('failed completion retains the draft and retry uses the same public seam',
ui.submit();
await settle(ui);
assert.equal(ui.input().props.value, 'retain this');
- assert.equal(ui.find(node => node.props?.role === 'alert').props.children, 'composer.send-failed');
+ assert.equal(module.hooks.notifications.at(-1).message, 'composer.send-failed');
ui.props.pageComposer = { execute: async () => ({ status: 'accepted' }) };
ui.render();
ui.submit();
@@ -401,7 +404,7 @@ test('a Host UTF16 admission rejection keeps the complete supplementary-characte
ui.submit();
await settle(ui);
assert.equal(ui.input().props.value, draft);
- assert.equal(ui.find(node => node.props?.role === 'alert').props.children, 'composer.send-failed');
+ assert.equal(module.hooks.notifications.at(-1).message, 'composer.send-failed');
});
test('uses the public controlled MarkdownEditor and imperative selection contract', () => {
diff --git a/test/chatroom-page-behavior.mjs b/test/chatroom-page-behavior.mjs
index 8851066..54cb48d 100644
--- a/test/chatroom-page-behavior.mjs
+++ b/test/chatroom-page-behavior.mjs
@@ -238,7 +238,7 @@ test('header action prevents duplicate execution and surfaces failure without ch
reject(new Error('failed'));
await new Promise(resolve => setImmediate(resolve));
tree = render();
- assert.equal(byClass(tree, 'cx-chatroom-page__error').props.children, 'page.action.failed');
+ assert.equal(harness.notifications.at(-1).message, 'page.action.failed');
});
test('inspector resize clamps pointer/keyboard width and preserves it across detail pages and close', async () => {
@@ -405,7 +405,7 @@ test('timeline menus offer detail/mention, keyboard dismissal and write-only cop
assert.deepEqual(copies, ['**exact text**'], 'writeText is invoked synchronously within the explicit click handler');
await new Promise(resolve => setImmediate(resolve));
tree = render();
- assert.equal(byClass(tree, 'cx-chatroom-timeline__feedback').props.children, 'timeline.copied');
+ assert.equal(harness.notifications.at(-1).message, 'timeline.copied');
byClass(message(), 'cx-chatroom-message__time').props.onClick({ currentTarget: trigger });
assert.deepEqual(copies.at(-1), item.timestamp);
await new Promise(resolve => setImmediate(resolve));
@@ -446,7 +446,7 @@ test('timeline copying is honestly disabled when absent and reports browser perm
message = all(tree, node => node.props?.item === item)[0];
byClass(message.type(message.props), 'cx-chatroom-message__time').props.onClick({ currentTarget: trigger });
await new Promise(resolve => setImmediate(resolve));
- assert.equal(byClass(render(), 'cx-chatroom-timeline__feedback').props.children, 'timeline.copy-failed');
+ assert.equal(harness.notifications.at(-1).message, 'timeline.copy-failed');
});
test('message actions preserve order, current execution, disabled reasons and duplicate-click fencing', async () => {
@@ -522,7 +522,7 @@ test('message actions preserve order, current execution, disabled reasons and du
rejectFirst(new Error('command failed'));
await new Promise(resolve => setImmediate(resolve));
tree = render();
- assert.equal(all(tree, node => node.props?.role === 'alert')[0].props.children, 'timeline.action-failed');
+ assert.equal(harness.notifications.at(-1).message, 'timeline.action-failed');
direct = all(message(tree), node => node.props?.className === 'cx-chatroom-message__command');
direct[0].props.onClick({ stopPropagation() {} });
await new Promise(resolve => setImmediate(resolve));
@@ -791,7 +791,7 @@ test('new Room uses the normal composer with an optional Leader selection, never
assert.deepEqual(await all(tree, node => node.type === 'Composer')[0].props.firstMessage('accepted'), {
status: 'accepted',
});
- assert.equal(all(render(), node => node.props?.role === 'status')[0].props.children, 'task.start.open-failed');
+ assert.equal(harness.notifications.at(-1).message, 'task.start.open-failed');
});
test('Room settings rejects backend-invalid names, prevents duplicate save and closes only on success', async () => {
diff --git a/test/entity-project-binding.mjs b/test/entity-project-binding.mjs
index 149758b..1a400b6 100644
--- a/test/entity-project-binding.mjs
+++ b/test/entity-project-binding.mjs
@@ -47,7 +47,7 @@ test('project binding retry retains the mutation and never starts a Session', as
assert.equal(h.requests.length, 2);
assert.deepEqual(h.requests[0], h.requests[1]);
assert.deepEqual(h.requests[0].binding, { kind: 'project', projectId: 'actual-project' });
- assert.equal(all(h.render(), node => node.props?.role === 'status')[0].props.children, 'detail.project-saved');
+ assert.equal(h.harness.notifications.at(-1).message, 'detail.project-saved');
h.harness.unmount();
});
diff --git a/test/entity-templates.mjs b/test/entity-templates.mjs
index f4d0f46..f9f4607 100644
--- a/test/entity-templates.mjs
+++ b/test/entity-templates.mjs
@@ -199,11 +199,11 @@ test('package pins the exact Protocol and Host revisions with exact manifest byt
const packageJson = JSON.parse(readFileSync(path.join(repositoryRoot, 'package.json'), 'utf8'));
assert.equal(
packageJson.devDependencies['@cordisx/protocol'],
- 'github:cordisx/cordisx-protocol#a39c5d1f61999a69043180d6b7e7bbbee3072c09',
+ 'github:cordisx/cordisx-protocol#f3e18c925c34d90fdf203c158811cef10f7ebf57',
);
assert.equal(
packageJson.devDependencies.cordisx,
- 'github:cordisx/cordisx#e0a7d7b6fbd653dcf55b4105d3d193e49443826f',
+ 'file:.cache/sdk/cordisx-efbff656d84b.tgz',
);
assert.equal(packageJson.main, packageManifest.entry);
assert.equal(packageManifest.entry, './dist/runtime/chatroom.js');
diff --git a/test/helpers/component-hook-harness.mjs b/test/helpers/component-hook-harness.mjs
index d877f69..b4dfd2c 100644
--- a/test/helpers/component-hook-harness.mjs
+++ b/test/helpers/component-hook-harness.mjs
@@ -7,6 +7,7 @@ export async function componentHarness(file, dependencies = {}) {
if (file === 'chatroom-timeline.tsx' && dependencies['./chatroom-timeline-entries.js'] === undefined) {
dependencies['./chatroom-timeline-entries.js'] = (await componentHarness('chatroom-timeline-entries.tsx')).exports;
}
+ const notifications = [];
const state = [];
const effects = [];
let index = 0;
@@ -47,6 +48,16 @@ export async function componentHarness(file, dependencies = {}) {
const require = name => {
if (name === './chatroom-room-actions.js') return { ChatroomRoomActions: 'RoomActions' };
if (name === './chatroom-message-body.js') return { ChatroomMessageBody: 'MessageBody' };
+ if (name === './notifications.js') {
+ return dependencies[name] ?? {
+ useNotifications: () => ({
+ show: options => {
+ notifications.push(options);
+ return { dismiss() {} };
+ },
+ }),
+ };
+ }
if (name === 'cordisx/react') return react;
if (name === './chatroom-inspector.js') {
return dependencies[name] ?? { useChatroomInspector: () => ({ width: 360, narrow: false, separatorProps: {} }) };
@@ -58,6 +69,7 @@ export async function componentHarness(file, dependencies = {}) {
new Function('require', 'exports', output)(require, exports);
return {
exports,
+ notifications,
render: (Component, props) => {
index = 0;
return Component(props);
diff --git a/test/real-cli-room.mjs b/test/real-cli-room.mjs
index 7ccef1d..e150d91 100644
--- a/test/real-cli-room.mjs
+++ b/test/real-cli-room.mjs
@@ -8,17 +8,21 @@ import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { Context } from '@deepseek-ai/cordis';
-import { OwnerDocumentStore } from '../node_modules/cordisx/packages/cli/dist/src/launcher/owner-document-store.js';
-import { createOwnerDocumentBridgeHandler } from '../node_modules/cordisx/packages/cli/dist/src/launcher/owner-document-rpc.js';
-import {
+const { OwnerDocumentStore } = await import(
+ new URL('./launcher/owner-document-store.js', import.meta.resolve('cordisx/contracts'))
+);
+const { createOwnerDocumentBridgeHandler } = await import(
+ new URL('./launcher/owner-document-rpc.js', import.meta.resolve('cordisx/contracts'))
+);
+const {
BrowserOwnerDocumentBridge,
CordisXOwnerDocumentBroker,
-} from '../node_modules/cordisx/packages/cli/dist/src/renderer/owner-documents.js';
-import {
+} = await import(new URL('./renderer/owner-documents.js', import.meta.resolve('cordisx/contracts')));
+const {
dispatchAgentTool,
getAgentToolSetup,
installAgentTools,
-} from '../node_modules/cordisx/packages/cli/dist/src/renderer/plugin-agent-tools.js';
+} = await import(new URL('./renderer/plugin-agent-tools.js', import.meta.resolve('cordisx/contracts')));
import { DurableChatroomRoomStore } from '../dist/room-store.js';
import { addRoomRun, bindRoomRunSession, createRoom } from '../dist/room.js';
import { ChatroomCliBindings } from '../dist/room-cli-bindings.js';
From 3634be2ada95ba2e7fe439a7535adc5225e9b45b Mon Sep 17 00:00:00 2001
From: YiJie
Date: Thu, 10 Sep 2026 03:32:52 +0800
Subject: [PATCH 2/2] Use merged notification providers through normal Git
dependencies
---
.agents/docs/notifications.md | 26 ++++----
AGENTS.md | 7 +--
README.md | 2 +-
notification-sdk-evidence.json | 50 ---------------
package-lock.json | 111 ++++++++++++++++++++++++---------
package.json | 4 +-
test/entity-templates.mjs | 4 +-
7 files changed, 100 insertions(+), 104 deletions(-)
delete mode 100644 notification-sdk-evidence.json
diff --git a/.agents/docs/notifications.md b/.agents/docs/notifications.md
index 3e41a3e..1a2e70a 100644
--- a/.agents/docs/notifications.md
+++ b/.agents/docs/notifications.md
@@ -6,26 +6,22 @@ Declare `notifications` in injection requirements. Field validation and persiste
business state stay beside the relevant control or object. Do not introduce a
custom Toast, page-wide operation error, or raw exception message.
-The [Host guide](https://github.com/cordisx/cordisx/blob/3158c0401a3f604727f623f1a8ac584017dd8540/.agents/docs/notifications.md)
+The [Host guide](https://github.com/cordisx/cordisx/blob/3cfe370eb7abf33e16686fbd82659cd441247fbd/.agents/docs/notifications.md)
owns usage and interaction guidance; the
-[Protocol contract](https://github.com/cordisx/cordisx-protocol/blob/dfa2c5fa956184df11a97955b8e5c74a76cb8876/.agents/docs/notifications-v1.md)
-owns the public API. This migration is an experimental candidate, not a release.
+[Protocol contract](https://github.com/cordisx/cordisx-protocol/blob/f46dd21e15a949a26f05f89bf11dea339fc60c02/.agents/docs/notifications-v1.md)
+owns the public API. Source merge and package publication are separate stages.
-## Reproduce the candidate SDK
+## Development dependencies
-The local SDK archive is deliberately excluded from Git. Its exact Host commit,
-Protocol source and hashes are recorded in [notification-sdk-evidence.json](../../notification-sdk-evidence.json).
-From a Host checkout at `efbff656d84b482d51598bc5ba303d24134e0c62`, run the
-[SDK builder](https://github.com/cordisx/cordisx/blob/main/.agents/docs/sdk-source-packaging.md)
-into a fresh directory, then run from this package:
+`package.json` and `package-lock.json` pin formally merged Host and Protocol
+commits. Install through the repository's normal dependency flow:
```sh
-mkdir -p .cache/sdk
-cp /path/to/sdk/packages/cordisx-0.1.0-beta.2.tgz .cache/sdk/cordisx-efbff656d84b.tgz
-npm ci --ignore-scripts
+npm ci
npm run check
```
-Compare the archive SHA-256 with the evidence before installation. Use one shared
-Protocol override; independent copies break branded public types. This explicit
-SDK build avoids recursive Git prepare scripts and does not change the API contract.
+Keep the shared Protocol override so public branded types resolve to one copy.
+A missing notification service requires a supported Host upgrade; do not recreate
+notification UI as an older-Host fallback. The former local candidate SDK archive
+is no longer an installation prerequisite.
diff --git a/AGENTS.md b/AGENTS.md
index dd87906..8fbd1f0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -22,10 +22,7 @@ manually positioned alert, or page-wide success/error paragraph. Keep field
validation and durable business state beside the relevant object. Use stable
semantic `kind` values, localized safe text, and notification rules owned by Host;
never expose raw exceptions or notify on every polling attempt.
-See the [Host notification guide](https://github.com/cordisx/cordisx/blob/3158c0401a3f604727f623f1a8ac584017dd8540/.agents/docs/notifications.md)
+See the [Host notification guide](https://github.com/cordisx/cordisx/blob/3cfe370eb7abf33e16686fbd82659cd441247fbd/.agents/docs/notifications.md)
for the interaction contract and older-Host capability boundary.
-The notification migration currently uses exact feature-branch SDK/Protocol
-revisions; it is not evidence of a formal release or a Mono pointer upgrade.
-
-Candidate setup: [notification migration](./.agents/docs/notifications.md).
+Dependency setup: [notification migration](./.agents/docs/notifications.md).
diff --git a/README.md b/README.md
index a7b83af..921ff8c 100644
--- a/README.md
+++ b/README.md
@@ -221,4 +221,4 @@ validation; that does not make them a merged or released dependency.
## Notification feedback
-See [operation notifications and candidate SDK setup](./.agents/docs/notifications.md).
+See [operation notifications and development dependencies](./.agents/docs/notifications.md).
diff --git a/notification-sdk-evidence.json b/notification-sdk-evidence.json
deleted file mode 100644
index 4bbcb09..0000000
--- a/notification-sdk-evidence.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "hostCommit": "efbff656d84b482d51598bc5ba303d24134e0c62",
- "node": "v24.14.1",
- "npm": "11.11.0",
- "sources": [
- {
- "location": "node_modules/@cordisx/channel",
- "spec": "git+ssh://git@github.com/cordisx/plugin-channel.git#4cee12e3a92eeed557bc9de8cc4792710918327a"
- },
- {
- "location": "node_modules/@cordisx/eslint-config",
- "spec": "git+ssh://git@github.com/cordisx/cordisxmono.git#c63c2e8c2ba7e11502934a52ad2ce3734e804cdc"
- },
- {
- "location": "node_modules/@cordisx/plugin-cli-proxy-api",
- "spec": "git+ssh://git@github.com/cordisx/plugin-cli-proxy-api.git#1428ee205aab31df2779398cc8491879b303d1d0"
- },
- {
- "location": "node_modules/@cordisx/protocol",
- "spec": "git+ssh://git@github.com/cordisx/cordisx-protocol.git#f3e18c925c34d90fdf203c158811cef10f7ebf57"
- }
- ],
- "packages": [
- {
- "filename": "cordisx-0.1.0-beta.2.tgz",
- "sha256": "c3ea96b691a8e2e6b52ced3cf1a54fa29830e8b29538112312e036e5d3b5b42a",
- "integrity": "sha512-f6wHDDz/2+71POc5WEaoiN7xsbHZJMpHob0pxnlH8YCpPAvU2uC8elNHOg7iasuwgry9QrAL5+hEOMRKc0nAgQ=="
- },
- {
- "filename": "cordisx-channel-0.1.0.tgz",
- "sha256": "ca3fa6283bd5a6af41faf60d936373412246beba047dd652924f5b92060f09e5",
- "integrity": "sha512-WqKvGxy72WwAUdwoLIHTBCG5/MAwVLKh2VmGK3tdSB+EMSUmuR+lAhxfdt9dYQRQL1Mh3t/w8SdYqps4mO0FkQ=="
- },
- {
- "filename": "cordisx-plugin-cli-proxy-api-0.1.0.tgz",
- "sha256": "31d23ed0aa49372f2a8b77fcf74e8e80ba0cc8abf19aed8dc06305246f9d7830",
- "integrity": "sha512-Otc61B3LTLweo0strMzH3XQH5I/P+HXAuFEFN+ynnnfvwscMj5BHovqAz2YS+JvPO79p+3WuCBTnEKh8DN/rkw=="
- },
- {
- "filename": "cordisx-protocol-0.1.0-alpha.0.tgz",
- "sha256": "04e72b3c58ccb6722a6c56459da1e70c11d0162760eda29bb4846233ea8bb8fe",
- "integrity": "sha512-o/YzvFs5gkrYsK0vijubLN/go2/oL04sixPz/Ks7N0eMLEHUL7qxD56Yf1ltW5dN9qfBIpgkHuiXgkSgOjj1bw=="
- },
- {
- "filename": "create-cordisx-plugin-0.1.0-beta.2.tgz",
- "sha256": "3e4d16e315d4a911aabeff3cf6e3b21d64d3d4be7d9b9b07de610443027a5d30",
- "integrity": "sha512-mdchmWwhZj/C938TpkiaNFjMhTFGzdYeG0bSk5AEhe+NdaoRmP3g0eUhu3DAfsvNNilp+vBbDmT4kWwcH9LwDg=="
- }
- ]
-}
diff --git a/package-lock.json b/package-lock.json
index f769784..90b8a1b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,7 @@
},
"devDependencies": {
"@cordisx/eslint-config": "github:cordisx/cordisxmono#c63c2e8c2ba7e11502934a52ad2ce3734e804cdc",
- "@cordisx/protocol": "github:cordisx/cordisx-protocol#f3e18c925c34d90fdf203c158811cef10f7ebf57",
+ "@cordisx/protocol": "github:cordisx/cordisx-protocol#f46dd21e15a949a26f05f89bf11dea339fc60c02",
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/schemastery": "3.18.1",
"@oneworks/avatar": "1.0.0-rc.9",
@@ -25,7 +25,7 @@
"@vitejs/plugin-react": "6.1.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
- "cordisx": "file:.cache/sdk/cordisx-efbff656d84b.tgz",
+ "cordisx": "github:cordisx/cordisx#3cfe370eb7abf33e16686fbd82659cd441247fbd",
"dprint": "0.57.1",
"esbuild": "^0.28.2",
"eslint": "9.39.4",
@@ -157,7 +157,7 @@
},
"node_modules/@cordisx/protocol": {
"version": "0.1.0-alpha.0",
- "resolved": "git+ssh://git@github.com/cordisx/cordisx-protocol.git#f3e18c925c34d90fdf203c158811cef10f7ebf57",
+ "resolved": "git+ssh://git@github.com/cordisx/cordisx-protocol.git#f46dd21e15a949a26f05f89bf11dea339fc60c02",
"integrity": "sha512-o/YzvFs5gkrYsK0vijubLN/go2/oL04sixPz/Ks7N0eMLEHUL7qxD56Yf1ltW5dN9qfBIpgkHuiXgkSgOjj1bw==",
"dev": true,
"license": "AGPL-3.0-or-later",
@@ -2776,9 +2776,10 @@
}
},
"node_modules/cordisx": {
+ "name": "cordisx-monorepo",
"version": "0.1.0-beta.2",
- "resolved": "file:.cache/sdk/cordisx-efbff656d84b.tgz",
- "integrity": "sha512-f6wHDDz/2+71POc5WEaoiN7xsbHZJMpHob0pxnlH8YCpPAvU2uC8elNHOg7iasuwgry9QrAL5+hEOMRKc0nAgQ==",
+ "resolved": "git+ssh://git@github.com/cordisx/cordisx.git#3cfe370eb7abf33e16686fbd82659cd441247fbd",
+ "integrity": "sha512-k6IhNsliOoWBDU0PuaQJUWO/llUdJAlrcdV9ErjyjSK+kYK0MJzO9UWWSnTc3bJGwLJ/Osv1cNo+tGV7BPwhcA==",
"bundleDependencies": [
"@cordisx/schemastery-ui",
"@cordisx/channel",
@@ -2786,10 +2787,13 @@
],
"dev": true,
"license": "AGPL-3.0-or-later",
+ "workspaces": [
+ "packages/*"
+ ],
"dependencies": {
- "@cordisx/channel": "0.1.0",
- "@cordisx/plugin-cli-proxy-api": "0.1.0",
- "@cordisx/protocol": "github:cordisx/cordisx-protocol#f3e18c925c34d90fdf203c158811cef10f7ebf57",
+ "@cordisx/channel": "github:cordisx/plugin-channel#4cee12e3a92eeed557bc9de8cc4792710918327a",
+ "@cordisx/plugin-cli-proxy-api": "github:cordisx/plugin-cli-proxy-api#1428ee205aab31df2779398cc8491879b303d1d0",
+ "@cordisx/protocol": "github:cordisx/cordisx-protocol#f46dd21e15a949a26f05f89bf11dea339fc60c02",
"@cordisx/schemastery-ui": "0.1.0-beta.2",
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/schemastery": "^3.18.1",
@@ -2799,7 +2803,6 @@
"@shikitor/core": "1.0.2",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.5",
- "@vitejs/plugin-react": "6.1.0",
"debug": "4.4.3",
"es-module-lexer": "1.7.0",
"esbuild": "0.28.2",
@@ -2822,34 +2825,15 @@
"swr": "2.5.1",
"tar": "7.5.22",
"tdesign-react": "1.18.2",
- "vite": "8.2.2",
"ws": "^8.18.3"
},
"bin": {
- "cordisx": "dist/src/cli.js"
+ "cordisx": "packages/cli/dist/src/cli.js"
},
"engines": {
"node": ">=22.19"
}
},
- "node_modules/cordisx/node_modules/@cordisx/channel": {
- "version": "0.1.0",
- "dev": true,
- "inBundle": true,
- "license": "AGPL-3.0-or-later",
- "engines": {
- "node": ">=22.19"
- }
- },
- "node_modules/cordisx/node_modules/@cordisx/plugin-cli-proxy-api": {
- "version": "0.1.0",
- "dev": true,
- "inBundle": true,
- "license": "AGPL-3.0-or-later",
- "engines": {
- "node": ">=22.19"
- }
- },
"node_modules/cordisx/node_modules/@cordisx/schemastery-ui": {
"version": "0.1.0-beta.2",
"dev": true,
@@ -2876,6 +2860,75 @@
"react-dom": ">=18.3.0 <20"
}
},
+ "node_modules/cordisx/packages/cli": {
+ "name": "cordisx",
+ "version": "0.1.0-beta.2",
+ "bundleDependencies": [
+ "@cordisx/schemastery-ui",
+ "@cordisx/channel",
+ "@cordisx/plugin-cli-proxy-api"
+ ],
+ "extraneous": true,
+ "license": "AGPL-3.0-or-later",
+ "dependencies": {
+ "@cordisx/channel": "0.1.0",
+ "@cordisx/plugin-cli-proxy-api": "0.1.0",
+ "@cordisx/protocol": "github:cordisx/cordisx-protocol#f46dd21e15a949a26f05f89bf11dea339fc60c02",
+ "@cordisx/schemastery-ui": "0.1.0-beta.2",
+ "@deepseek-ai/cordis": "4.0.1",
+ "@deepseek-ai/schemastery": "^3.18.1",
+ "@larksuiteoapi/node-sdk": "^1.73.0",
+ "@oneworks/avatar": "1.0.0-rc.8",
+ "@oneworks/avatar-react": "1.0.0-rc.8",
+ "@shikitor/core": "1.0.2",
+ "@types/react": "19.2.18",
+ "@types/react-dom": "19.2.5",
+ "@vitejs/plugin-react": "6.1.0",
+ "debug": "4.4.3",
+ "es-module-lexer": "1.7.0",
+ "esbuild": "0.28.2",
+ "extend": "3.0.2",
+ "intl-messageformat": "11.2.14",
+ "lightningcss": "1.33.0",
+ "luna-console": "1.3.6",
+ "luna-data-grid": "1.6.5",
+ "luna-dom-viewer": "1.8.4",
+ "luna-object-viewer": "0.3.2",
+ "react": "19.2.8",
+ "react-dom": "19.2.8",
+ "react-markdown": "10.1.0",
+ "rehype-raw": "^7.0.0",
+ "rehype-sanitize": "^6.0.0",
+ "reicon": "1.2.1",
+ "remark-gfm": "4.0.1",
+ "shiki": "4.4.3",
+ "style-to-js": "1.1.21",
+ "swr": "2.5.1",
+ "tar": "7.5.22",
+ "tdesign-react": "1.18.2",
+ "vite": "8.2.2",
+ "ws": "^8.18.3"
+ },
+ "bin": {
+ "cordisx": "dist/src/cli.js"
+ },
+ "devDependencies": {
+ "@cordisx/channel-runtime": "file:../channel-runtime",
+ "@types/node": "^22.18.1",
+ "@types/react": "19.2.18",
+ "@types/react-dom": "19.2.5",
+ "@types/ws": "^8.18.1",
+ "ajv": "8.20.0",
+ "ajv-formats": "3.0.1",
+ "jsdom": "^26.1.0",
+ "tsx": "^4.20.5",
+ "typescript": "^5.9.2",
+ "vitest": "^3.2.4"
+ },
+ "engines": {
+ "node": ">=22.19"
+ }
+ },
"node_modules/cosmiconfig": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
diff --git a/package.json b/package.json
index a7b338a..598ed48 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
},
"devDependencies": {
"@cordisx/eslint-config": "github:cordisx/cordisxmono#c63c2e8c2ba7e11502934a52ad2ce3734e804cdc",
- "@cordisx/protocol": "github:cordisx/cordisx-protocol#f3e18c925c34d90fdf203c158811cef10f7ebf57",
+ "@cordisx/protocol": "github:cordisx/cordisx-protocol#f46dd21e15a949a26f05f89bf11dea339fc60c02",
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/schemastery": "3.18.1",
"@oneworks/avatar": "1.0.0-rc.9",
@@ -47,7 +47,7 @@
"@vitejs/plugin-react": "6.1.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
- "cordisx": "file:.cache/sdk/cordisx-efbff656d84b.tgz",
+ "cordisx": "github:cordisx/cordisx#3cfe370eb7abf33e16686fbd82659cd441247fbd",
"dprint": "0.57.1",
"esbuild": "^0.28.2",
"eslint": "9.39.4",
diff --git a/test/entity-templates.mjs b/test/entity-templates.mjs
index f9f4607..88ab05a 100644
--- a/test/entity-templates.mjs
+++ b/test/entity-templates.mjs
@@ -199,11 +199,11 @@ test('package pins the exact Protocol and Host revisions with exact manifest byt
const packageJson = JSON.parse(readFileSync(path.join(repositoryRoot, 'package.json'), 'utf8'));
assert.equal(
packageJson.devDependencies['@cordisx/protocol'],
- 'github:cordisx/cordisx-protocol#f3e18c925c34d90fdf203c158811cef10f7ebf57',
+ 'github:cordisx/cordisx-protocol#f46dd21e15a949a26f05f89bf11dea339fc60c02',
);
assert.equal(
packageJson.devDependencies.cordisx,
- 'file:.cache/sdk/cordisx-efbff656d84b.tgz',
+ 'github:cordisx/cordisx#3cfe370eb7abf33e16686fbd82659cd441247fbd',
);
assert.equal(packageJson.main, packageManifest.entry);
assert.equal(packageManifest.entry, './dist/runtime/chatroom.js');