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

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.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ dist/
coverage/
.DS_Store
cordisx.config.local.json

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

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

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -64,5 +64,8 @@
},
"bin": {
"cordisx-chatroom": "./dist/agent-tools/cli/chatroom.mjs"
},
"overrides": {
"@cordisx/protocol": "$@cordisx/protocol"
}
}
15 changes: 8 additions & 7 deletions src/chatroom-composer.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<string>();
const notifications = useNotifications();
const [error, setError] = useState<string>();
const [query, setQuery] = useState<string>();
const [activeIndex, setActiveIndex] = useState(0);
Expand Down Expand Up @@ -231,7 +232,6 @@ export function ChatroomComposer(
setSending(true);
setQuery(undefined);
setError(undefined);
setNotice(undefined);
try {
if (firstMessage !== undefined) {
const result = await firstMessage(submittedDraft);
Expand All @@ -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);
Expand Down Expand Up @@ -355,7 +357,6 @@ export function ChatroomComposer(
onValueChange={value => {
draftRef.current = value;
setDraft(value);
setNotice(undefined);
setError(undefined);
updateQuery(value, input.current?.getSelection());
}}
Expand Down Expand Up @@ -414,7 +415,7 @@ export function ChatroomComposer(
{t(shortcutPolicy === 'enter' ? 'composer.shortcut.enter' : 'composer.shortcut.mod-enter')}
</p>
<div id={`${id}-status`} role="status" className="cx-chatroom-input__status">
{unavailable ? t('composer.unavailable') : sending ? t('composer.sending') : notice}
{unavailable ? t('composer.unavailable') : sending ? t('composer.sending') : undefined}
</div>
{error !== undefined && <p role="alert" className="cx-chatroom-input__error">{error}</p>}
</form>
Expand Down
16 changes: 10 additions & 6 deletions src/chatroom-entity-settings.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(() => {});
Expand All @@ -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);
Expand All @@ -50,7 +55,6 @@ export function ChatroomEntitySettings({ room, participantId, details, t }: {
>
<Icon name="host:settings" aria-hidden="true" />
</Button>
{failed && <p role="alert">{t('identity.settings.failed')}</p>}
</div>
);
}
11 changes: 6 additions & 5 deletions src/chatroom-member-details.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -20,7 +21,7 @@ export function ChatroomMemberDetails({ snapshot, participantId, details, t }: {
const [entity, setEntity] = useState<EntityRecord>();
const [loading, setLoading] = useState(true);
const [opening, setOpening] = useState<string>();
const [error, setError] = useState(false);
const notifications = useNotifications();
const room = snapshot.room;
const member = room?.memberships.find(candidate => candidate.participantId === participantId);
useEffect(() => {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -102,7 +104,6 @@ export function ChatroomMemberDetails({ snapshot, participantId, details, t }: {
))}
</ul>
)}
{error && <p role="alert">{t('identity.session.open-failed')}</p>}
</section>
{unassignedTasks.length > 0 && (
<section>
Expand Down
Loading
Loading