Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1efb71b
Simplify discussion UI
felixfeng33 Apr 5, 2026
239c962
Polish suggestion line break
felixfeng33 Apr 5, 2026
33ea5e5
Fix suggestion removal regressions
felixfeng33 Apr 6, 2026
45552fb
Fix editor suggestion regressions
felixfeng33 Apr 6, 2026
04293a4
Restore agent metadata
felixfeng33 Apr 6, 2026
927e023
refactor(www): inline void suggestion helpers
felixfeng33 Apr 6, 2026
4ddde6c
fix(www): sync registry helper deps
felixfeng33 Apr 6, 2026
7f4adcd
fix(core): mark react entry client
felixfeng33 Apr 6, 2026
bd42d86
fix(plate): move react client boundary
felixfeng33 Apr 6, 2026
cc652a2
refactor(www): simplify discussion click targets
felixfeng33 Apr 7, 2026
8f52efb
fix(suggestion): keep whitespace on reject
felixfeng33 Apr 8, 2026
96b8a4c
fix(suggestion): handle inline deletion regressions
felixfeng33 Apr 9, 2026
868c268
fix(suggestion): handle inline void range replacement
felixfeng33 Apr 10, 2026
017c010
chore: checkpoint app registry boundary investigation
felixfeng33 Apr 13, 2026
a3129fe
fix(www): make trailing block kit client-only
felixfeng33 Apr 13, 2026
54707d5
fix(registry): sync template discussion and ai helpers
felixfeng33 Apr 13, 2026
babb589
fix(suggestion): handle non-selectable date voids
felixfeng33 Apr 15, 2026
1bba8be
fix(suggestion): narrow date delete fallback
felixfeng33 Apr 15, 2026
09837cf
fix(suggestion): avoid line-break removes on block voids
felixfeng33 Apr 16, 2026
7563822
fix(ci): align suggestion mocks with runtime helpers
felixfeng33 Apr 16, 2026
979a819
test(www): move inline void suggestion spec to slow lane
felixfeng33 Apr 16, 2026
2fbf17e
fix(www): move void remove suggestions below root
felixfeng33 Apr 16, 2026
6f41f19
chore: restore root prepare script
felixfeng33 Apr 16, 2026
094c9f0
fix
felixfeng33 Apr 22, 2026
239c4a2
fix(www): simplify suggestion registry wiring
felixfeng33 Apr 25, 2026
6ef3ee4
Merge remote-tracking branch 'origin/main' into codex/simplify-discus…
felixfeng33 Apr 25, 2026
c6976f8
test(www): isolate static date suggestion mock
felixfeng33 Apr 25, 2026
4ea7930
test(www): move slow registry specs out of fast suite
felixfeng33 Apr 25, 2026
29774f8
chore: remove tracked omx state
felixfeng33 Apr 26, 2026
4b4ca54
chore(www): disable preview deployments
zbeyens Apr 26, 2026
50f4c1c
chore: disable vercel previews from root config
zbeyens Apr 26, 2026
14ad11f
refactor(www): align registry node prop style
felixfeng33 Apr 27, 2026
4606d15
fix(www): split inline suggestion registry helper
felixfeng33 Apr 27, 2026
8b98598
fix(www): restore inline suggestion self styling
felixfeng33 Apr 27, 2026
3123d22
refactor(www): use inline suggestion variants directly
felixfeng33 Apr 28, 2026
e674027
chore(www): revert generated registry artifacts
felixfeng33 Apr 28, 2026
f54190e
refactor(www): rename inline suggestion helper
felixfeng33 Apr 28, 2026
4c0e00a
chore: update release notes
felixfeng33 Apr 28, 2026
59e0883
Merge remote-tracking branch 'origin/main' into codex/simplify-discus…
felixfeng33 Apr 29, 2026
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
5 changes: 5 additions & 0 deletions .changeset/ai-chat-stop-clears-stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@platejs/ai": patch
---

Clear block streaming state when `aiChat.stop()` stops generation
5 changes: 5 additions & 0 deletions .changeset/link-empty-remove-normalize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@platejs/link": patch
---

Fix empty link normalization when suggestion acceptance removes the last link character
5 changes: 5 additions & 0 deletions .changeset/suggestion-mention-backspace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@platejs/suggestion": patch
---

Fix inline-void delete and replace suggestions around mentions and paragraph boundaries
5 changes: 5 additions & 0 deletions .changeset/utils-trailing-block-insert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@platejs/utils": patch
---

Add a trailing-block insert hook for normalization-driven insert behavior
169 changes: 169 additions & 0 deletions apps/www/src/__tests__/package-integration/suggestion-link.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/** @jsx jsxt */

import {
acceptSuggestion,
BaseSuggestionPlugin,
getSuggestionKey,
rejectSuggestion,
} from '@platejs/suggestion';
import { jsxt } from '@platejs/test-utils';
import type { SlateEditor } from 'platejs';
import { createSlateEditor } from 'platejs';

import { BaseEditorKit } from '@/registry/components/editor/editor-base-kit';

jsxt;

const createEditor = (input: SlateEditor) =>
createSlateEditor({
plugins: BaseEditorKit,
selection: input.selection,
value: input.children,
} as any);

describe('suggestion link integration', () => {
it('marks only the previous link character when deleting backward after a link', () => {
const input = (
<editor>
<hp>
<htext>before </htext>
<ha url="https://example.com">link</ha>
<htext>
<cursor />
{' after'}
</htext>
</hp>
</editor>
) as any as SlateEditor;

const output = (
<editor>
<hp>
<htext>before </htext>
<ha url="https://example.com">
<htext>lin</htext>
<htext
suggestion
suggestion_1={{
id: 'placeholder',
createdAt: 0,
type: 'remove',
userId: 'alice',
}}
>
<cursor />k
</htext>
</ha>
<htext>{' after'}</htext>
</hp>
</editor>
) as any as SlateEditor;

const editor = createEditor(input);
editor.setOption(BaseSuggestionPlugin, 'isSuggesting', true);

editor.tf.deleteBackward();

const outputLinkNode = output.children[0].children[1] as any;
const linkNode = editor.children[0].children[1] as any;
const suggestionLeaf = linkNode.children[1] as any;
const suggestionData = editor
.getApi(BaseSuggestionPlugin)
.suggestion.suggestionData(suggestionLeaf) as any;

expect(editor.children[0].children[0]).toEqual(
output.children[0].children[0]
);
expect(linkNode.children[0]).toEqual(outputLinkNode.children[0]);
expect(suggestionLeaf.text).toBe(outputLinkNode.children[1].text);
expect(suggestionData?.type).toBe('remove');
expect(suggestionData?.userId).toBe('alice');
expect(linkNode.suggestion).toBeUndefined();
expect(
Object.keys(linkNode).filter((key) => key.startsWith('suggestion_'))
).toHaveLength(0);
expect(editor.children[0].children[2]).toEqual(
output.children[0].children[2]
);
expect(editor.selection).toEqual(output.selection);
});

it('removes an empty link after accepting the last removed character', () => {
const removeData = {
id: '1',
createdAt: Date.now(),
type: 'remove',
userId: 'alice',
};

const input = (
<editor>
<hp>
before{' '}
<ha url="https://reactjs.org">
<htext suggestion_1={removeData} suggestion>
t
</htext>
</ha>
</hp>
</editor>
) as any as SlateEditor;

const output = (
<editor>
<hp>before </hp>
</editor>
) as any as SlateEditor;

const editor = createEditor(input);
editor.selection = {
anchor: { offset: 1, path: [0, 1, 0] },
focus: { offset: 1, path: [0, 1, 0] },
};

acceptSuggestion(editor, {
keyId: getSuggestionKey('1'),
suggestionId: '1',
} as any);

expect(editor.children).toEqual(output.children);
});

it('rejects remove suggestion on inline link elements', () => {
const removeData = {
id: '1',
createdAt: Date.now(),
type: 'remove',
userId: 'alice',
};

const input = (
<editor>
<hp>
before{' '}
<ha suggestion suggestion_1={removeData} url="https://example.com">
link
</ha>{' '}
after
</hp>
</editor>
) as any as SlateEditor;

const output = (
<editor>
<hp>
before <ha url="https://example.com">link</ha> after
</hp>
</editor>
) as any as SlateEditor;

const editor = createEditor(input);

rejectSuggestion(editor, {
keyId: 'suggestion_1',
suggestionId: '1',
} as any);

expect(editor.children).toEqual(output.children);
});
});
5 changes: 1 addition & 4 deletions apps/www/src/registry/components/editor/plugins/ai-kit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,7 @@ export const aiChatPlugin = AIChatPlugin.extend({
}
},
onFinish: () => {
editor.setOption(AIChatPlugin, 'streaming', false);
editor.setOption(AIChatPlugin, '_blockChunks', '');
editor.setOption(AIChatPlugin, '_blockPath', null);
editor.setOption(AIChatPlugin, '_mdxName', null);
editor.getApi(AIChatPlugin).aiChat.stop();
},
});
},
Expand Down
44 changes: 13 additions & 31 deletions apps/www/src/registry/components/editor/plugins/comment-kit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,63 +7,45 @@ import {
BaseCommentPlugin,
getDraftCommentKey,
} from '@platejs/comment';
import { isSlateString } from 'platejs';
import { toTPlatePlugin } from 'platejs/react';

import { CommentLeaf } from '@/registry/ui/comment-node';
import { getDiscussionClickTarget } from './discussion-kit';

type CommentConfig = ExtendConfig<
BaseCommentConfig,
{
activeId: string | null;
commentingBlock: Path | null;
hoverId: string | null;
uniquePathMap: Map<string, Path>;
}
>;

export const commentPlugin = toTPlatePlugin<CommentConfig>(BaseCommentPlugin, {
handlers: {
onClick: ({ api, event, setOption, type }) => {
let leaf = event.target as HTMLElement;
let isSet = false;
const activeTarget = getDiscussionClickTarget({
selector: `.slate-${type}`,
target: event.target,
});

const unsetActiveSuggestion = () => {
if (!activeTarget) {
setOption('activeId', null);
isSet = true;
};

if (!isSlateString(leaf)) unsetActiveSuggestion();

while (leaf.parentElement) {
if (leaf.classList.contains(`slate-${type}`)) {
const commentsEntry = api.comment!.node();

if (!commentsEntry) {
unsetActiveSuggestion();

break;
}

const id = api.comment!.nodeId(commentsEntry[0]);

setOption('activeId', id ?? null);
isSet = true;

break;
}

leaf = leaf.parentElement;
return;
}

if (!isSet) unsetActiveSuggestion();
const commentEntry = api.comment?.node();

setOption(
'activeId',
commentEntry ? (api.comment?.nodeId(commentEntry[0]) ?? null) : null
);
},
},
options: {
activeId: null,
commentingBlock: null,
hoverId: null,
uniquePathMap: new Map(),
},
})
.extendTransforms(
Expand Down
35 changes: 35 additions & 0 deletions apps/www/src/registry/components/editor/plugins/discussion-kit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,41 @@ export type TDiscussion = {
documentContent?: string;
};

const BLOCK_SUGGESTION_SELECTOR = '[data-block-suggestion="true"]';

const getTargetElement = (target: EventTarget | null) => {
if (target instanceof HTMLElement) return target;
if (target instanceof Node) return target.parentElement;

return null;
};

export const getDiscussionClickTarget = ({
selector,
target,
}: {
selector: string;
target: EventTarget | null;
}) => {
const element = getTargetElement(target);

if (!element) return null;

return element.closest(selector) as HTMLElement | null;
};

export const getDiscussionBlockClickTarget = ({
selector = BLOCK_SUGGESTION_SELECTOR,
target,
}: {
selector?: string;
target: EventTarget | null;
}) =>
getDiscussionClickTarget({
selector,
target,
});

const discussionsData: TDiscussion[] = [
{
id: 'discussion1',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { afterAll, describe, expect, it, mock } from 'bun:test';

mock.module('@platejs/suggestion', () => ({
BaseSuggestionPlugin: {
configure: (config: any) => config,
},
}));

mock.module('platejs', () => ({
KEYS: {
date: 'date',
inlineEquation: 'inline_equation',
link: 'link',
mention: 'mention',
},
TextApi: {
isText: (node: any) => typeof node?.text === 'string',
},
}));

mock.module('@/registry/ui/suggestion-node-static', () => ({
SuggestionLeafStatic: () => null,
VoidRemoveSuggestionOverlayStatic: () => null,
}));

describe('BaseSuggestionKit', () => {
afterAll(() => {
mock.restore();
});

it('injects inline suggestion type for static inline element rendering', async () => {
const { BaseSuggestionKit } = await import(
`./suggestion-base-kit?test=${Math.random().toString(36).slice(2)}`
);

const transformProps = (BaseSuggestionKit[0] as any).inject.nodeProps
.transformProps;
const editor = {
getApi: () => ({
suggestion: {
dataList: (node: any) =>
Object.keys(node)
.filter((key) => key.startsWith('suggestion_'))
.map((key) => node[key]),
suggestionData: (element: any) => element.suggestion,
},
}),
};

expect(
transformProps({
editor,
element: {
children: [
{
suggestion_1: {
createdAt: 0,
id: 'suggestion-1',
type: 'remove',
userId: 'alice',
},
text: '',
},
],
type: 'date',
},
props: {},
})
).toEqual({
'data-inline-suggestion': 'remove',
});
});
});
Loading
Loading