Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .agents/docs/notifications.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ node_modules/
dist/
coverage/
.DS_Store

.cache/sdk/
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
41 changes: 30 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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"
}
}
4 changes: 2 additions & 2 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -202,7 +202,7 @@ export function apply(ctx: Context): void {
});

const manager = managerFor(ctx);
const mount = defineReactPage<Messages>(createChannelPage(manager));
const mount = defineReactPage<Messages>(createChannelPage(manager, ctx.notifications));
for (const metadata of pages.map(page)) ctx.pages.register<Messages>(metadata, mount);
for (const definition of routes.map(route)) ctx.routes.register(definition);

Expand Down
6 changes: 6 additions & 0 deletions src/locales.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export type CopyKey =
| "operation.saved"
| "operation.failed"
| "accounts"
| "accounts.empty"
| "create"
Expand All @@ -25,6 +27,8 @@ export type CopyKey =
| "status.state";

const EN: Record<CopyKey, string> = {
"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",
Expand Down Expand Up @@ -52,6 +56,8 @@ const EN: Record<CopyKey, string> = {
};

const ZH: Record<CopyKey, string> = {
"operation.saved": "操作已完成",
"operation.failed": "操作未完成,请检查连接后重试",
accounts: "渠道账号",
"accounts.empty": "暂无可用渠道账号。",
create: "新建渠道",
Expand Down
10 changes: 10 additions & 0 deletions src/notifications.ts
Original file line number Diff line number Diff line change
@@ -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"),
});
}
3 changes: 2 additions & 1 deletion src/page-types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 3 additions & 3 deletions src/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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";
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 <ChannelList {...shared} />;
if (props.routeId.endsWith(":create")) return <ChannelCreate {...shared} />;
if (props.routeId.endsWith(":runtime")) return <ChannelRuntime {...shared} />;
Expand Down
7 changes: 4 additions & 3 deletions src/pages/configuration.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
Expand All @@ -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);
}
Expand All @@ -47,7 +49,6 @@ export function ChannelConfiguration(props: ChannelPageProps) {
<input value={name} onChange={event => setName(event.currentTarget.value)} />
</label>
<div className="cxc-channel-actions">
<span className="cxc-channel-note" role="status">{status}</span>
{(["connection.enable", "connection.disable", "connection.reconnect"] as const).map(operation => (
<Button
key={operation}
Expand Down
7 changes: 4 additions & 3 deletions src/pages/create.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { notifyResult } from "../notifications.js";
import { type FormEvent, useState } from "cordisx/react";
import { Button, Icon, Stack, Text } from "cordisx/ui";
import type { ChannelPageProps } from "../page-types.js";
Expand All @@ -10,7 +11,6 @@ export function ChannelCreate(props: ChannelPageProps) {
const [platform, setPlatform] = useState<"simulator" | "feishu" | "lark">("simulator");
const [name, setName] = useState("");
const [selectors, setSelectors] = useState<readonly ("direct" | "group")[]>(["direct"]);
const [status, setStatus] = useState("");
const [busy, setBusy] = useState(false);
const available = canCreateConnection(snapshot, platform);

Expand All @@ -26,7 +26,9 @@ export function ChannelCreate(props: ChannelPageProps) {
});
if (result === "applied") {
await props.navigation.navigate({ id: "settings" });
} else setStatus(copy(props.locale, "create.unavailable"));
} else notifyResult(props, "connection.create", false);
} catch {
notifyResult(props, "connection.create", false);
} finally {
setBusy(false);
}
Expand Down Expand Up @@ -59,7 +61,6 @@ export function ChannelCreate(props: ChannelPageProps) {
</label>
</Stack>
<div className="cxc-channel-actions">
<span className="cxc-channel-note" role="status">{status}</span>
<Button variant="primary" type="submit" disabled={busy || !available || name.trim() === ""}>
<Icon name="success" />
{copy(props.locale, "create.save")}
Expand Down
Loading