diff --git a/.agents/docs/notifications.md b/.agents/docs/notifications.md new file mode 100644 index 0000000..1a2e70a --- /dev/null +++ b/.agents/docs/notifications.md @@ -0,0 +1,27 @@ +# 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/3cfe370eb7abf33e16686fbd82659cd441247fbd/.agents/docs/notifications.md) +owns usage and interaction guidance; the +[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. + +## Development dependencies + +`package.json` and `package-lock.json` pin formally merged Host and Protocol +commits. Install through the repository's normal dependency flow: + +```sh +npm ci +npm run check +``` + +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/.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..8fbd1f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,3 +13,16 @@ 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/3cfe370eb7abf33e16686fbd82659cd441247fbd/.agents/docs/notifications.md) +for the interaction contract and older-Host capability boundary. + +Dependency setup: [notification migration](./.agents/docs/notifications.md). diff --git a/README.md b/README.md index 8696efc..921ff8c 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 development dependencies](./.agents/docs/notifications.md). diff --git a/package-lock.json b/package-lock.json index b3b5203..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#a39c5d1f61999a69043180d6b7e7bbbee3072c09", + "@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": "github:cordisx/cordisx#e0a7d7b6fbd653dcf55b4105d3d193e49443826f", + "cordisx": "github:cordisx/cordisx#3cfe370eb7abf33e16686fbd82659cd441247fbd", "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#f46dd21e15a949a26f05f89bf11dea339fc60c02", + "integrity": "sha512-o/YzvFs5gkrYsK0vijubLN/go2/oL04sixPz/Ks7N0eMLEHUL7qxD56Yf1ltW5dN9qfBIpgkHuiXgkSgOjj1bw==", "dev": true, "license": "AGPL-3.0-or-later", "engines": { @@ -2798,10 +2778,12 @@ "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": "git+ssh://git@github.com/cordisx/cordisx.git#3cfe370eb7abf33e16686fbd82659cd441247fbd", + "integrity": "sha512-k6IhNsliOoWBDU0PuaQJUWO/llUdJAlrcdV9ErjyjSK+kYK0MJzO9UWWSnTc3bJGwLJ/Osv1cNo+tGV7BPwhcA==", "bundleDependencies": [ - "@cordisx/schemastery-ui" + "@cordisx/schemastery-ui", + "@cordisx/channel", + "@cordisx/plugin-cli-proxy-api" ], "dev": true, "license": "AGPL-3.0-or-later", @@ -2811,7 +2793,7 @@ "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/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", @@ -2882,14 +2864,16 @@ "name": "cordisx", "version": "0.1.0-beta.2", "bundleDependencies": [ - "@cordisx/schemastery-ui" + "@cordisx/schemastery-ui", + "@cordisx/channel", + "@cordisx/plugin-cli-proxy-api" ], "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/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", @@ -2930,7 +2914,6 @@ }, "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", @@ -7783,27 +7766,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..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#a39c5d1f61999a69043180d6b7e7bbbee3072c09", + "@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": "github:cordisx/cordisx#e0a7d7b6fbd653dcf55b4105d3d193e49443826f", + "cordisx": "github:cordisx/cordisx#3cfe370eb7abf33e16686fbd82659cd441247fbd", "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 }: { >