diff --git a/.agents/docs/notifications.md b/.agents/docs/notifications.md new file mode 100644 index 0000000..4b999e6 --- /dev/null +++ b/.agents/docs/notifications.md @@ -0,0 +1,27 @@ +# Operation notifications + +Connection configuration, creation and log export 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 b044cb7..38128f4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/ dist/ coverage/ .DS_Store + +.cache/sdk/ diff --git a/AGENTS.md b/AGENTS.md index 2f023df..f94dc70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,3 +7,16 @@ - Use only versioned public Protocol and Host exports. Never copy Host-private renderer, launcher, service-config, credential, DOM, or adapter implementation into this repository. - Keep Host chrome, routing history, credential capture, downloads, accessibility primitives, and native integration Host-owned. - Run `npm run check` before delivery. + +## 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 62f3012..2fe247a 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,7 @@ filesystem paths, DOM handles, or transport callbacks. The package remains private at version `0.1.0` to prevent accidental publication before the Host-stamped service configuration revision and final consumer removal are complete. + +## Notification feedback + +See [operation notifications and development dependencies](./.agents/docs/notifications.md). diff --git a/package-lock.json b/package-lock.json index 12d60bc..ea8a4a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,13 @@ "license": "AGPL-3.0-or-later", "devDependencies": { "@cordisx/eslint-config": "github:cordisx/cordisxmono#c63c2e8c2ba7e11502934a52ad2ce3734e804cdc", - "@cordisx/protocol": "github:cordisx/cordisx-protocol#3f0dbcd8b04ae83c920d2d913ac2c313af5f83f1", + "@cordisx/protocol": "github:cordisx/cordisx-protocol#f46dd21e15a949a26f05f89bf11dea339fc60c02", "@deepseek-ai/cordis": "4.0.1", "@projectwallace/stylelint-plugin": "0.7.0", "@typescript-eslint/parser": "8.69.0", "ajv": "8.20.0", "ajv-formats": "3.0.1", - "cordisx": "github:cordisx/cordisx#dfb071e02eca0ef52f84d67b2393c25aced7d3f0", + "cordisx": "github:cordisx/cordisx#3cfe370eb7abf33e16686fbd82659cd441247fbd", "dprint": "0.57.1", "esbuild": "0.28.2", "eslint": "9.39.4", @@ -143,8 +143,8 @@ }, "node_modules/@cordisx/protocol": { "version": "0.1.0-alpha.0", - "resolved": "git+ssh://git@github.com/cordisx/cordisx-protocol.git#3f0dbcd8b04ae83c920d2d913ac2c313af5f83f1", - "integrity": "sha512-/7FUbWT/N3nswpyimUWRzLvAinMpvbCoQ8AFRR0+NlAlQQs1K5GEhwKAp6lmyG+TF240ZynQvSqzg0y7yc+b8A==", + "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": { @@ -2396,10 +2396,12 @@ "node_modules/cordisx": { "name": "cordisx-monorepo", "version": "0.1.0-beta.2", - "resolved": "git+ssh://git@github.com/cordisx/cordisx.git#dfb071e02eca0ef52f84d67b2393c25aced7d3f0", - "integrity": "sha512-L+a3aDxbGWcUZKApU2sLVqEdjDCgbvfqjiVmPoXe6WNQBYQb4veqKQQcq29NLd0zZS4WCKLS2Gza7gE8ZoWVIw==", + "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", @@ -2407,7 +2409,9 @@ "packages/*" ], "dependencies": { - "@cordisx/protocol": "github:cordisx/cordisx-protocol#3f0dbcd8b04ae83c920d2d913ac2c313af5f83f1", + "@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", @@ -2461,12 +2465,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/protocol": "github:cordisx/cordisx-protocol#3f0dbcd8b04ae83c920d2d913ac2c313af5f83f1", + "@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", @@ -2507,7 +2515,6 @@ }, "devDependencies": { "@cordisx/channel-runtime": "file:../channel-runtime", - "@cordisx/protocol": "github:cordisx/cordisx-protocol#3f0dbcd8b04ae83c920d2d913ac2c313af5f83f1", "@types/node": "^22.18.1", "@types/react": "19.2.18", "@types/react-dom": "19.2.5", @@ -4321,6 +4328,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4342,6 +4352,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4363,6 +4376,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4384,6 +4400,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index 73d4b13..4637a91 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@cordisx/eslint-config": "github:cordisx/cordisxmono#c63c2e8c2ba7e11502934a52ad2ce3734e804cdc", - "@cordisx/protocol": "github:cordisx/cordisx-protocol#3f0dbcd8b04ae83c920d2d913ac2c313af5f83f1", + "@cordisx/protocol": "github:cordisx/cordisx-protocol#f46dd21e15a949a26f05f89bf11dea339fc60c02", "@projectwallace/stylelint-plugin": "0.7.0", "@deepseek-ai/cordis": "4.0.1", "@typescript-eslint/parser": "8.69.0", @@ -51,7 +51,7 @@ "stylelint": "17.15.0", "stylelint-config-standard": "40.0.0", "typescript": "5.9.2", - "cordisx": "github:cordisx/cordisx#dfb071e02eca0ef52f84d67b2393c25aced7d3f0", + "cordisx": "github:cordisx/cordisx#3cfe370eb7abf33e16686fbd82659cd441247fbd", "esbuild": "0.28.2", "react": "19.2.8", "react-dom": "19.2.8" @@ -63,5 +63,8 @@ "bugs": { "url": "https://github.com/cordisx/plugin-channel/issues" }, - "homepage": "https://github.com/cordisx/plugin-channel#readme" + "homepage": "https://github.com/cordisx/plugin-channel#readme", + "overrides": { + "@cordisx/protocol": "$@cordisx/protocol" + } } diff --git a/src/channel.ts b/src/channel.ts index 8780b98..68c9594 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -17,7 +17,7 @@ import { defineReactPage } from "cordisx/react"; import { createChannelPage } from "./page.js"; export const name = "channel"; -export const inject = ["i18n", "slots", "pages", "routes", "managerContent", "channelManager"]; +export const inject = ["i18n", "slots", "pages", "routes", "managerContent", "channelManager", "notifications"]; export const manifest = { $schema: CORDISX_PLUGIN_MANIFEST_SCHEMA_V8, @@ -202,7 +202,7 @@ export function apply(ctx: Context): void { }); const manager = managerFor(ctx); - const mount = defineReactPage(createChannelPage(manager)); + const mount = defineReactPage(createChannelPage(manager, ctx.notifications)); for (const metadata of pages.map(page)) ctx.pages.register(metadata, mount); for (const definition of routes.map(route)) ctx.routes.register(definition); diff --git a/src/locales.ts b/src/locales.ts index fd02da6..37ea3e6 100644 --- a/src/locales.ts +++ b/src/locales.ts @@ -1,4 +1,6 @@ export type CopyKey = + | "operation.saved" + | "operation.failed" | "accounts" | "accounts.empty" | "create" @@ -25,6 +27,8 @@ export type CopyKey = | "status.state"; const EN: Record = { + "operation.saved": "Operation completed.", + "operation.failed": "Operation failed. Check the connection and try again.", accounts: "Channel accounts", "accounts.empty": "No channel accounts are available.", create: "New channel", @@ -52,6 +56,8 @@ const EN: Record = { }; const ZH: Record = { + "operation.saved": "操作已完成", + "operation.failed": "操作未完成,请检查连接后重试", accounts: "渠道账号", "accounts.empty": "暂无可用渠道账号。", create: "新建渠道", diff --git a/src/notifications.ts b/src/notifications.ts new file mode 100644 index 0000000..2510ce3 --- /dev/null +++ b/src/notifications.ts @@ -0,0 +1,10 @@ +import type { ChannelPageProps } from "./page-types.js"; +import { copy } from "./locales.js"; + +export function notifyResult(props: ChannelPageProps, kind: string, success: boolean): void { + props.notifications.show({ + kind, + type: success ? "success" : "error", + message: copy(props.locale, success ? "operation.saved" : "operation.failed"), + }); +} diff --git a/src/page-types.ts b/src/page-types.ts index 4f42874..8364ddc 100644 --- a/src/page-types.ts +++ b/src/page-types.ts @@ -1,7 +1,8 @@ import type { ChannelManagerV2 } from "@cordisx/protocol/channel-manager/v2"; -import type { CordisXReactPageProps } from "cordisx/contracts"; +import type { CordisXReactPageProps, NotificationsV1 } from "cordisx/contracts"; export interface ChannelPageProps extends CordisXReactPageProps { + readonly notifications: NotificationsV1; readonly manager: ChannelManagerV2; readonly locale: string; } diff --git a/src/page.tsx b/src/page.tsx index 084a9d8..ff46e4e 100644 --- a/src/page.tsx +++ b/src/page.tsx @@ -1,5 +1,5 @@ import type { ChannelManagerV2 } from "@cordisx/protocol/channel-manager/v2"; -import type { CordisXReactPageProps } from "cordisx/contracts"; +import type { CordisXReactPageProps, NotificationsV1 } from "cordisx/contracts"; import { ChannelConfiguration } from "./pages/configuration.js"; import { ChannelCreate } from "./pages/create.js"; import { ChannelList } from "./pages/list.js"; @@ -7,9 +7,9 @@ import { ChannelLogs } from "./pages/logs.js"; import { ChannelRuntime } from "./pages/runtime.js"; import { ChannelSessions } from "./pages/sessions.js"; -export function createChannelPage(manager: ChannelManagerV2) { +export function createChannelPage(manager: ChannelManagerV2, notifications: NotificationsV1) { return function ChannelPage(props: CordisXReactPageProps) { - const shared = { ...props, manager, locale: props.localization.getSnapshot().locale }; + const shared = { ...props, manager, notifications, locale: props.localization.getSnapshot().locale }; if (props.routeId.endsWith(":settings")) return ; if (props.routeId.endsWith(":create")) return ; if (props.routeId.endsWith(":runtime")) return ; diff --git a/src/pages/configuration.tsx b/src/pages/configuration.tsx index fbb02ff..0c77b2a 100644 --- a/src/pages/configuration.tsx +++ b/src/pages/configuration.tsx @@ -1,3 +1,4 @@ +import { notifyResult } from "../notifications.js"; import { useState } from "cordisx/react"; import { Button, EmptyState, Stack, Text } from "cordisx/ui"; import { copy } from "../locales.js"; @@ -9,7 +10,6 @@ export function ChannelConfiguration(props: ChannelPageProps) { const { manager, snapshot } = useChannelModel(props.manager); const account = accountByToken(snapshot, routeToken(props.params.connectionToken)); const [name, setName] = useState(account?.displayName ?? ""); - const [status, setStatus] = useState(""); const [busy, setBusy] = useState(false); if (account === undefined) { return ( @@ -30,7 +30,9 @@ export function ChannelConfiguration(props: ChannelPageProps) { ? { ...operationFence(snapshot), operation, target, patch: { displayName: name.trim() } } : { ...operationFence(snapshot), operation, target }, ); - setStatus(result.status); + notifyResult(props, operation, result.status === "applied"); + } catch { + notifyResult(props, operation, false); } finally { setBusy(false); } @@ -47,7 +49,6 @@ export function ChannelConfiguration(props: ChannelPageProps) { setName(event.currentTarget.value)} />
- {status} {(["connection.enable", "connection.disable", "connection.reconnect"] as const).map(operation => (
- {status} {entries.length === 0 ? : (