@ai-composer/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +6 -0
- package/dist/commands/builtins.d.ts +41 -0
- package/dist/commands/builtins.d.ts.map +1 -0
- package/dist/commands/builtins.js +116 -0
- package/dist/commands/builtins.js.map +1 -0
- package/dist/commands/registry.d.ts +27 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +29 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/editor.d.ts +185 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +558 -0
- package/dist/editor.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +22 -0
- package/dist/errors.js.map +1 -0
- package/dist/events/event-bus.d.ts +19 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +51 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/events/events.d.ts +94 -0
- package/dist/events/events.d.ts.map +1 -0
- package/dist/events/events.js +6 -0
- package/dist/events/events.js.map +1 -0
- package/dist/factory.d.ts +17 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +18 -0
- package/dist/factory.js.map +1 -0
- package/dist/history/history.d.ts +48 -0
- package/dist/history/history.d.ts.map +1 -0
- package/dist/history/history.js +71 -0
- package/dist/history/history.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/model/document.d.ts +54 -0
- package/dist/model/document.d.ts.map +1 -0
- package/dist/model/document.js +177 -0
- package/dist/model/document.js.map +1 -0
- package/dist/model/nodes.d.ts +80 -0
- package/dist/model/nodes.d.ts.map +1 -0
- package/dist/model/nodes.js +64 -0
- package/dist/model/nodes.js.map +1 -0
- package/dist/model/selection.d.ts +40 -0
- package/dist/model/selection.d.ts.map +1 -0
- package/dist/model/selection.js +79 -0
- package/dist/model/selection.js.map +1 -0
- package/dist/nodes/registry.d.ts +39 -0
- package/dist/nodes/registry.d.ts.map +1 -0
- package/dist/nodes/registry.js +93 -0
- package/dist/nodes/registry.js.map +1 -0
- package/dist/package.json +21 -0
- package/dist/plugins/registry.d.ts +53 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/plugins/registry.js +102 -0
- package/dist/plugins/registry.js.map +1 -0
- package/dist/serialization/builtins.d.ts +30 -0
- package/dist/serialization/builtins.d.ts.map +1 -0
- package/dist/serialization/builtins.js +102 -0
- package/dist/serialization/builtins.js.map +1 -0
- package/dist/serialization/index.d.ts +4 -0
- package/dist/serialization/index.d.ts.map +1 -0
- package/dist/serialization/index.js +3 -0
- package/dist/serialization/index.js.map +1 -0
- package/dist/serialization/registry.d.ts +17 -0
- package/dist/serialization/registry.d.ts.map +1 -0
- package/dist/serialization/registry.js +35 -0
- package/dist/serialization/registry.js.map +1 -0
- package/dist/serialization/serializers.d.ts +12 -0
- package/dist/serialization/serializers.d.ts.map +1 -0
- package/dist/serialization/serializers.js +3 -0
- package/dist/serialization/serializers.js.map +1 -0
- package/dist/state/state.d.ts +54 -0
- package/dist/state/state.d.ts.map +1 -0
- package/dist/state/state.js +6 -0
- package/dist/state/state.js.map +1 -0
- package/dist/triggers/engine.d.ts +102 -0
- package/dist/triggers/engine.d.ts.map +1 -0
- package/dist/triggers/engine.js +235 -0
- package/dist/triggers/engine.js.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/html.d.ts +3 -0
- package/dist/utils/html.d.ts.map +1 -0
- package/dist/utils/html.js +10 -0
- package/dist/utils/html.js.map +1 -0
- package/package.json +20 -0
- package/project.json +56 -0
- package/src/commands/builtins.ts +142 -0
- package/src/commands/registry.ts +53 -0
- package/src/commands.test.ts +118 -0
- package/src/editor.test.ts +311 -0
- package/src/editor.ts +826 -0
- package/src/errors.ts +25 -0
- package/src/events/event-bus.test.ts +62 -0
- package/src/events/event-bus.ts +72 -0
- package/src/events/events.ts +120 -0
- package/src/factory.ts +21 -0
- package/src/history/history.ts +108 -0
- package/src/index.ts +201 -0
- package/src/model/document.test.ts +130 -0
- package/src/model/document.ts +207 -0
- package/src/model/nodes.ts +151 -0
- package/src/model/selection.ts +107 -0
- package/src/nodes/registry.ts +142 -0
- package/src/plugins/registry.ts +163 -0
- package/src/serialization/builtins.ts +123 -0
- package/src/serialization/index.ts +20 -0
- package/src/serialization/registry.ts +49 -0
- package/src/serialization/serializers.ts +15 -0
- package/src/state/state.ts +57 -0
- package/src/triggers/engine.ts +317 -0
- package/src/triggers.test.ts +138 -0
- package/src/types.ts +42 -0
- package/src/utils/html.ts +9 -0
- package/tsconfig.json +15 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.spec.json +16 -0
- package/vitest.config.ts +11 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Typed error codes so adapters and plugins can branch on failures. */
|
|
2
|
+
export const ErrorCode = {
|
|
3
|
+
EDITOR_DESTROYED: 'EDITOR_DESTROYED',
|
|
4
|
+
UNKNOWN_COMMAND: 'UNKNOWN_COMMAND',
|
|
5
|
+
UNKNOWN_FORMAT: 'UNKNOWN_FORMAT',
|
|
6
|
+
UNKNOWN_TRIGGER: 'UNKNOWN_TRIGGER',
|
|
7
|
+
PLUGIN_SETUP_FAILED: 'PLUGIN_SETUP_FAILED',
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type AIComposerErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
11
|
+
|
|
12
|
+
export class AIComposerError extends Error {
|
|
13
|
+
readonly code: AIComposerErrorCode;
|
|
14
|
+
|
|
15
|
+
constructor(code: AIComposerErrorCode, message: string) {
|
|
16
|
+
super(`[${code}] ${message}`);
|
|
17
|
+
this.name = 'AIComposerError';
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function toError(value: unknown): Error {
|
|
23
|
+
if (value instanceof Error) return value;
|
|
24
|
+
return new Error(typeof value === 'string' ? value : JSON.stringify(value));
|
|
25
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createEventBus } from './event-bus';
|
|
3
|
+
|
|
4
|
+
describe('event bus', () => {
|
|
5
|
+
it('subscribes and emits typed payloads', () => {
|
|
6
|
+
const bus = createEventBus<{ change: { value: number }; focus: { focused: boolean } }>();
|
|
7
|
+
const handler = vi.fn();
|
|
8
|
+
bus.on('change', handler);
|
|
9
|
+
bus.emit('change', { value: 42 });
|
|
10
|
+
expect(handler).toHaveBeenCalledWith({ value: 42 });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('unsubscribe stops delivery', () => {
|
|
14
|
+
const bus = createEventBus<{ ping: { n: number } }>();
|
|
15
|
+
const handler = vi.fn();
|
|
16
|
+
const off = bus.on('ping', handler);
|
|
17
|
+
off();
|
|
18
|
+
bus.emit('ping', { n: 1 });
|
|
19
|
+
expect(handler).not.toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('once fires a single time', () => {
|
|
23
|
+
const bus = createEventBus<{ ping: { n: number } }>();
|
|
24
|
+
const handler = vi.fn();
|
|
25
|
+
bus.once('ping', handler);
|
|
26
|
+
bus.emit('ping', { n: 1 });
|
|
27
|
+
bus.emit('ping', { n: 2 });
|
|
28
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('off removes a specific handler', () => {
|
|
32
|
+
const bus = createEventBus<{ ping: { n: number } }>();
|
|
33
|
+
const a = vi.fn();
|
|
34
|
+
const b = vi.fn();
|
|
35
|
+
bus.on('ping', a);
|
|
36
|
+
bus.on('ping', b);
|
|
37
|
+
bus.off('ping', a);
|
|
38
|
+
bus.emit('ping', { n: 1 });
|
|
39
|
+
expect(a).not.toHaveBeenCalled();
|
|
40
|
+
expect(b).toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('a throwing listener does not break other listeners', () => {
|
|
44
|
+
const bus = createEventBus<{ ping: { n: number } }>();
|
|
45
|
+
const good = vi.fn();
|
|
46
|
+
bus.on('ping', () => {
|
|
47
|
+
throw new Error('boom');
|
|
48
|
+
});
|
|
49
|
+
bus.on('ping', good);
|
|
50
|
+
bus.emit('ping', { n: 1 });
|
|
51
|
+
expect(good).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('counts listeners and clears', () => {
|
|
55
|
+
const bus = createEventBus<{ ping: { n: number } }>();
|
|
56
|
+
bus.on('ping', () => undefined);
|
|
57
|
+
bus.on('ping', () => undefined);
|
|
58
|
+
expect(bus.listenerCount('ping')).toBe(2);
|
|
59
|
+
bus.clear();
|
|
60
|
+
expect(bus.listenerCount('ping')).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal typed event bus. No framework emitters in core — adapters map this
|
|
3
|
+
* to synthetic events, RxJS, component events, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type Unsubscribe = () => void;
|
|
7
|
+
export type EventHandler<TPayload> = (payload: TPayload) => void;
|
|
8
|
+
|
|
9
|
+
export interface EventMapBase {
|
|
10
|
+
[eventType: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface EventBus<TEvents extends EventMapBase> {
|
|
14
|
+
on<K extends keyof TEvents & string>(type: K, handler: EventHandler<TEvents[K]>): Unsubscribe;
|
|
15
|
+
once<K extends keyof TEvents & string>(type: K, handler: EventHandler<TEvents[K]>): Unsubscribe;
|
|
16
|
+
off<K extends keyof TEvents & string>(type: K, handler: EventHandler<TEvents[K]>): void;
|
|
17
|
+
emit<K extends keyof TEvents & string>(type: K, payload: TEvents[K]): void;
|
|
18
|
+
listenerCount<K extends keyof TEvents & string>(type: K): number;
|
|
19
|
+
clear(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createEventBus<TEvents extends EventMapBase>(): EventBus<TEvents> {
|
|
23
|
+
const handlers = new Map<string, Set<EventHandler<unknown>>>();
|
|
24
|
+
|
|
25
|
+
const bus: EventBus<TEvents> = {
|
|
26
|
+
on(type, handler) {
|
|
27
|
+
let set = handlers.get(type);
|
|
28
|
+
if (!set) {
|
|
29
|
+
set = new Set();
|
|
30
|
+
handlers.set(type, set);
|
|
31
|
+
}
|
|
32
|
+
set.add(handler as EventHandler<unknown>);
|
|
33
|
+
return () => bus.off(type, handler);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
once(type, handler) {
|
|
37
|
+
const wrapped: EventHandler<unknown> = (payload) => {
|
|
38
|
+
off();
|
|
39
|
+
(handler as EventHandler<unknown>)(payload);
|
|
40
|
+
};
|
|
41
|
+
const off = bus.on(type, wrapped as never);
|
|
42
|
+
return off;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
off(type, handler) {
|
|
46
|
+
handlers.get(type)?.delete(handler as EventHandler<unknown>);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
emit(type, payload) {
|
|
50
|
+
const set = handlers.get(type);
|
|
51
|
+
if (!set || set.size === 0) return;
|
|
52
|
+
for (const handler of [...set]) {
|
|
53
|
+
try {
|
|
54
|
+
handler(payload);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// A broken listener must never break the editor loop.
|
|
57
|
+
console.error(`[ai-composer] unhandled error in "${type}" listener`, error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
listenerCount(type) {
|
|
63
|
+
return handlers.get(type)?.size ?? 0;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
clear() {
|
|
67
|
+
handlers.clear();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return bus;
|
|
72
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor event contract. Every adapter emits exactly these semantics, so
|
|
3
|
+
* behavior can be contract-tested across frameworks.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EventBus } from './event-bus';
|
|
7
|
+
import type { AIComposerDocument } from '../model/document';
|
|
8
|
+
import type { AttachmentNode, AIComposerNode } from '../model/nodes';
|
|
9
|
+
import type { SelectionState } from '../model/selection';
|
|
10
|
+
import type { SuggestionItem } from '../state/state';
|
|
11
|
+
|
|
12
|
+
/** What caused a document change. */
|
|
13
|
+
export type ChangeSource = 'init' | 'user' | 'api' | 'plugin' | 'undo' | 'redo';
|
|
14
|
+
|
|
15
|
+
export interface ChangeEvent {
|
|
16
|
+
value: AIComposerDocument;
|
|
17
|
+
source: ChangeSource;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FocusChangeEvent {
|
|
21
|
+
focused: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BeforeSubmitEvent {
|
|
25
|
+
value: AIComposerDocument;
|
|
26
|
+
defaultPrevented: boolean;
|
|
27
|
+
preventDefault(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SubmitEvent {
|
|
31
|
+
value: AIComposerDocument;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SelectionChangeEvent {
|
|
35
|
+
selection: SelectionState;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TriggerOpenEvent {
|
|
39
|
+
triggerId: string;
|
|
40
|
+
query: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type TriggerCloseReason =
|
|
44
|
+
| 'select'
|
|
45
|
+
| 'escape'
|
|
46
|
+
| 'blur'
|
|
47
|
+
| 'input'
|
|
48
|
+
| 'manual'
|
|
49
|
+
| 'editor-destroy';
|
|
50
|
+
|
|
51
|
+
export interface TriggerCloseEvent {
|
|
52
|
+
triggerId: string;
|
|
53
|
+
reason: TriggerCloseReason;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SuggestionsChangeEvent {
|
|
57
|
+
triggerId: string | null;
|
|
58
|
+
suggestions: SuggestionItem[];
|
|
59
|
+
activeIndex: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface NodeInsertEvent {
|
|
63
|
+
node: AIComposerNode;
|
|
64
|
+
index: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface NodeRemoveEvent {
|
|
68
|
+
node: AIComposerNode;
|
|
69
|
+
index: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AttachmentEvent {
|
|
73
|
+
attachment: AttachmentNode;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CommandExecuteEvent {
|
|
77
|
+
id: string;
|
|
78
|
+
payload?: unknown;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ModeChangeEvent {
|
|
82
|
+
mode: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ErrorEvent {
|
|
86
|
+
error: Error;
|
|
87
|
+
/** Where it happened, e.g. `trigger:mention:search`, `submit`. */
|
|
88
|
+
phase?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface DestroyEvent {
|
|
92
|
+
target: 'editor';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type AIComposerEventMap = {
|
|
96
|
+
/** Any value change, from any source. */
|
|
97
|
+
change: ChangeEvent;
|
|
98
|
+
/** Value change caused by user input (typing, paste, chip insert). */
|
|
99
|
+
input: ChangeEvent;
|
|
100
|
+
focus: FocusChangeEvent;
|
|
101
|
+
blur: FocusChangeEvent;
|
|
102
|
+
beforeSubmit: BeforeSubmitEvent;
|
|
103
|
+
submit: SubmitEvent;
|
|
104
|
+
selectionChange: SelectionChangeEvent;
|
|
105
|
+
triggerOpen: TriggerOpenEvent;
|
|
106
|
+
triggerClose: TriggerCloseEvent;
|
|
107
|
+
suggestionsChange: SuggestionsChangeEvent;
|
|
108
|
+
nodeInsert: NodeInsertEvent;
|
|
109
|
+
nodeRemove: NodeRemoveEvent;
|
|
110
|
+
attachmentAdd: AttachmentEvent;
|
|
111
|
+
attachmentRemove: AttachmentEvent;
|
|
112
|
+
commandExecute: CommandExecuteEvent;
|
|
113
|
+
modeChange: ModeChangeEvent;
|
|
114
|
+
error: ErrorEvent;
|
|
115
|
+
destroy: DestroyEvent;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type AIComposerEventType = keyof AIComposerEventMap & string;
|
|
119
|
+
|
|
120
|
+
export type AIComposerBus = EventBus<AIComposerEventMap>;
|
package/src/factory.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Factory + version. */
|
|
2
|
+
|
|
3
|
+
import { AIComposerImpl, type AIComposer } from './editor';
|
|
4
|
+
import type { AIComposerOptions } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a framework-independent AI Composer editor instance.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const editor = createAIComposer({
|
|
12
|
+
* placeholder: 'Ask anything…',
|
|
13
|
+
* plugins: [mentionPlugin({ trigger: '@', items })],
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function createAIComposer(options: AIComposerOptions = {}): AIComposer {
|
|
18
|
+
return new AIComposerImpl(options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const CORE_VERSION = '0.1.0';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undo/redo — snapshot-based history (ADR-0003). Composer documents are small,
|
|
3
|
+
* so storing immutable document references beats operational transforms in
|
|
4
|
+
* simplicity and debuggability. Rapid consecutive user edits (typing) within
|
|
5
|
+
* `mergeWindowMs` collapse into a single undo step.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AIComposerDocument } from '../model/document';
|
|
9
|
+
import type { SelectionState } from '../model/selection';
|
|
10
|
+
|
|
11
|
+
export interface HistorySnapshot {
|
|
12
|
+
document: AIComposerDocument;
|
|
13
|
+
selection: SelectionState;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface HistoryEntry extends HistorySnapshot {
|
|
17
|
+
label: string;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface HistoryOptions {
|
|
22
|
+
/** Max undo steps kept (default 200). */
|
|
23
|
+
limit?: number;
|
|
24
|
+
/** Window in ms within which same-label user edits merge (default 500). */
|
|
25
|
+
mergeWindowMs?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_HISTORY_OPTIONS: Required<HistoryOptions> = {
|
|
29
|
+
limit: 200,
|
|
30
|
+
mergeWindowMs: 500,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface RecordOptions {
|
|
34
|
+
/** Change label — merges only happen between identical labels. */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** Only same-label merges; requires the caller to opt in (user typing). */
|
|
37
|
+
merge?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class EditorHistory {
|
|
41
|
+
private options: Required<HistoryOptions>;
|
|
42
|
+
/** States we can go back TO. */
|
|
43
|
+
private undoStack: HistoryEntry[] = [];
|
|
44
|
+
/** States we can go forward TO. */
|
|
45
|
+
private redoStack: HistoryEntry[] = [];
|
|
46
|
+
|
|
47
|
+
constructor(options: HistoryOptions = {}) {
|
|
48
|
+
this.options = { ...DEFAULT_HISTORY_OPTIONS, ...options };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get canUndo(): boolean {
|
|
52
|
+
return this.undoStack.length > 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get canRedo(): boolean {
|
|
56
|
+
return this.redoStack.length > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Record a change. `previous` is the state BEFORE the change; documents are
|
|
61
|
+
* immutable so no cloning is needed.
|
|
62
|
+
*/
|
|
63
|
+
record(previous: HistorySnapshot, options: RecordOptions = {}): void {
|
|
64
|
+
const label = options.label ?? 'edit';
|
|
65
|
+
this.redoStack = [];
|
|
66
|
+
|
|
67
|
+
const top = this.undoStack[this.undoStack.length - 1];
|
|
68
|
+
const shouldMerge =
|
|
69
|
+
options.merge === true &&
|
|
70
|
+
top !== undefined &&
|
|
71
|
+
top.label === label &&
|
|
72
|
+
Date.now() - top.timestamp <= this.options.mergeWindowMs;
|
|
73
|
+
|
|
74
|
+
if (!shouldMerge) {
|
|
75
|
+
this.undoStack.push({ ...previous, label, timestamp: Date.now() });
|
|
76
|
+
if (this.undoStack.length > this.options.limit) this.undoStack.shift();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Return the state to restore, or null when nothing to undo. */
|
|
81
|
+
undo(current: HistorySnapshot): HistorySnapshot | null {
|
|
82
|
+
const entry = this.undoStack.pop();
|
|
83
|
+
if (!entry) return null;
|
|
84
|
+
this.redoStack.push({ ...current, label: entry.label, timestamp: Date.now() });
|
|
85
|
+
return { document: entry.document, selection: entry.selection };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Return the state to restore, or null when nothing to redo. */
|
|
89
|
+
redo(current: HistorySnapshot): HistorySnapshot | null {
|
|
90
|
+
const entry = this.redoStack.pop();
|
|
91
|
+
if (!entry) return null;
|
|
92
|
+
this.undoStack.push({ ...current, label: entry.label, timestamp: Date.now() });
|
|
93
|
+
return { document: entry.document, selection: entry.selection };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setOptions(options: HistoryOptions): void {
|
|
97
|
+
this.options = { ...this.options, ...options };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getOptions(): Required<HistoryOptions> {
|
|
101
|
+
return { ...this.options };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
reset(): void {
|
|
105
|
+
this.undoStack = [];
|
|
106
|
+
this.redoStack = [];
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ai-composer/core — the framework-independent AI composer engine.
|
|
3
|
+
*
|
|
4
|
+
* Layers:
|
|
5
|
+
* model/ document, nodes, selection (pure data + edit primitives)
|
|
6
|
+
* state/ observable editor state
|
|
7
|
+
* events/ typed event bus + editor event map
|
|
8
|
+
* commands/ command registry + built-ins
|
|
9
|
+
* triggers/ trigger registry + suggestion engine
|
|
10
|
+
* history/ snapshot undo/redo with merge window
|
|
11
|
+
* plugins/ plugin contract + registry
|
|
12
|
+
* serialization/ json / text / markdown / html / ai serializers
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// model
|
|
16
|
+
export type { AIComposerDocument } from './model/document';
|
|
17
|
+
export {
|
|
18
|
+
coerceDocument,
|
|
19
|
+
createDocument,
|
|
20
|
+
deleteDocumentRange,
|
|
21
|
+
documentLength,
|
|
22
|
+
documentsEqual,
|
|
23
|
+
findNode,
|
|
24
|
+
findNodeIndex,
|
|
25
|
+
getNodesOfType,
|
|
26
|
+
insertNodesAt,
|
|
27
|
+
isEmptyDocument,
|
|
28
|
+
nodeLength,
|
|
29
|
+
normalizeNodes,
|
|
30
|
+
sanitizeDocument,
|
|
31
|
+
} from './model/document';
|
|
32
|
+
export type {
|
|
33
|
+
AttachmentNode,
|
|
34
|
+
AtomicNode,
|
|
35
|
+
BaseNode,
|
|
36
|
+
CommandNode,
|
|
37
|
+
CustomNode,
|
|
38
|
+
MentionNode,
|
|
39
|
+
NodeKey,
|
|
40
|
+
AIComposerNode,
|
|
41
|
+
AIComposerNodeType,
|
|
42
|
+
TextNode,
|
|
43
|
+
VariableNode,
|
|
44
|
+
} from './model/nodes';
|
|
45
|
+
export {
|
|
46
|
+
createAttachmentNode,
|
|
47
|
+
createCommandNode,
|
|
48
|
+
createCustomNode,
|
|
49
|
+
createMentionNode,
|
|
50
|
+
createNodeKey,
|
|
51
|
+
createTextNode,
|
|
52
|
+
createVariableNode,
|
|
53
|
+
ensureNodeKey,
|
|
54
|
+
getNodeType,
|
|
55
|
+
isAtomicNode,
|
|
56
|
+
isAIComposerNode,
|
|
57
|
+
isTextNode,
|
|
58
|
+
} from './model/nodes';
|
|
59
|
+
export type { DocumentRange, Position, SelectionState } from './model/selection';
|
|
60
|
+
export {
|
|
61
|
+
clampPosition,
|
|
62
|
+
clampSelection,
|
|
63
|
+
comparePositions,
|
|
64
|
+
createPosition,
|
|
65
|
+
createSelection,
|
|
66
|
+
equalsPosition,
|
|
67
|
+
equalsSelection,
|
|
68
|
+
fromGlobalOffset,
|
|
69
|
+
getEnd,
|
|
70
|
+
getRange,
|
|
71
|
+
getStart,
|
|
72
|
+
globalRange,
|
|
73
|
+
isCollapsed,
|
|
74
|
+
toGlobalOffset,
|
|
75
|
+
} from './model/selection';
|
|
76
|
+
|
|
77
|
+
// state
|
|
78
|
+
export type { AIComposerState, SuggestionItem, TriggerState } from './state/state';
|
|
79
|
+
|
|
80
|
+
// events
|
|
81
|
+
export type {
|
|
82
|
+
AttachmentEvent,
|
|
83
|
+
BeforeSubmitEvent,
|
|
84
|
+
ChangeEvent,
|
|
85
|
+
ChangeSource,
|
|
86
|
+
CommandExecuteEvent,
|
|
87
|
+
DestroyEvent,
|
|
88
|
+
ErrorEvent,
|
|
89
|
+
FocusChangeEvent,
|
|
90
|
+
ModeChangeEvent,
|
|
91
|
+
NodeInsertEvent,
|
|
92
|
+
NodeRemoveEvent,
|
|
93
|
+
AIComposerBus,
|
|
94
|
+
AIComposerEventMap,
|
|
95
|
+
AIComposerEventType,
|
|
96
|
+
SelectionChangeEvent,
|
|
97
|
+
SubmitEvent,
|
|
98
|
+
SuggestionsChangeEvent,
|
|
99
|
+
TriggerCloseEvent,
|
|
100
|
+
TriggerCloseReason,
|
|
101
|
+
TriggerOpenEvent,
|
|
102
|
+
} from './events/events';
|
|
103
|
+
export type {
|
|
104
|
+
EventBus,
|
|
105
|
+
EventHandler,
|
|
106
|
+
EventMapBase,
|
|
107
|
+
Unsubscribe,
|
|
108
|
+
} from './events/event-bus';
|
|
109
|
+
export { createEventBus } from './events/event-bus';
|
|
110
|
+
|
|
111
|
+
// commands
|
|
112
|
+
export type { CommandContext, CommandRegistry, AIComposerCommand } from './commands/registry';
|
|
113
|
+
export { createCommandRegistry } from './commands/registry';
|
|
114
|
+
export {
|
|
115
|
+
BUILTIN_COMMANDS,
|
|
116
|
+
createBuiltinCommands,
|
|
117
|
+
type AcceptSuggestionPayload,
|
|
118
|
+
type InsertNodePayload,
|
|
119
|
+
type InsertTextPayload,
|
|
120
|
+
type OpenTriggerPayload,
|
|
121
|
+
type RemoveNodePayload,
|
|
122
|
+
} from './commands/builtins';
|
|
123
|
+
|
|
124
|
+
// triggers
|
|
125
|
+
export type {
|
|
126
|
+
AIComposerTrigger,
|
|
127
|
+
TriggerHost,
|
|
128
|
+
TriggerRegistry,
|
|
129
|
+
TriggerSearchContext,
|
|
130
|
+
TriggerSelectContext,
|
|
131
|
+
} from './triggers/engine';
|
|
132
|
+
export {
|
|
133
|
+
TriggerEngine,
|
|
134
|
+
createNodeFromSuggestion,
|
|
135
|
+
createTriggerRegistry,
|
|
136
|
+
defineTrigger,
|
|
137
|
+
} from './triggers/engine';
|
|
138
|
+
|
|
139
|
+
// history
|
|
140
|
+
export type { HistoryOptions, HistorySnapshot, RecordOptions } from './history/history';
|
|
141
|
+
export { DEFAULT_HISTORY_OPTIONS, EditorHistory } from './history/history';
|
|
142
|
+
|
|
143
|
+
// plugins
|
|
144
|
+
export type { PluginContext, AIComposerPlugin, PluginRegistry } from './plugins/registry';
|
|
145
|
+
export { createPluginRegistry } from './plugins/registry';
|
|
146
|
+
|
|
147
|
+
// serialization
|
|
148
|
+
export type {
|
|
149
|
+
AIComposerPayload,
|
|
150
|
+
SerializationFormat,
|
|
151
|
+
Serializer,
|
|
152
|
+
SerializerContext,
|
|
153
|
+
SerializerRegistry,
|
|
154
|
+
} from './serialization';
|
|
155
|
+
export {
|
|
156
|
+
createAiSerializer,
|
|
157
|
+
createDefaultSerializers,
|
|
158
|
+
createHtmlSerializer,
|
|
159
|
+
createJsonSerializer,
|
|
160
|
+
createMarkdownSerializer,
|
|
161
|
+
createSerializerRegistry,
|
|
162
|
+
createTextSerializer,
|
|
163
|
+
documentToText,
|
|
164
|
+
nodeToText,
|
|
165
|
+
} from './serialization';
|
|
166
|
+
|
|
167
|
+
// node registry
|
|
168
|
+
export type { NodeDefinition, NodeRegistry } from './nodes/registry';
|
|
169
|
+
export {
|
|
170
|
+
ATTACHMENT_NODE_DEFINITION,
|
|
171
|
+
COMMAND_NODE_DEFINITION,
|
|
172
|
+
CUSTOM_NODE_DEFINITION,
|
|
173
|
+
MENTION_NODE_DEFINITION,
|
|
174
|
+
TEXT_NODE_DEFINITION,
|
|
175
|
+
VARIABLE_NODE_DEFINITION,
|
|
176
|
+
createDefaultNodeRegistry,
|
|
177
|
+
createNodeRegistry,
|
|
178
|
+
} from './nodes/registry';
|
|
179
|
+
|
|
180
|
+
// editor + factory + config
|
|
181
|
+
export type {
|
|
182
|
+
AIComposer,
|
|
183
|
+
EditorView,
|
|
184
|
+
InsertNodeOptions,
|
|
185
|
+
SetValueOptions,
|
|
186
|
+
StateListener,
|
|
187
|
+
TransactionOptions,
|
|
188
|
+
} from './editor';
|
|
189
|
+
export { AIComposerImpl, isAIComposer } from './editor';
|
|
190
|
+
export { createAIComposer, CORE_VERSION } from './factory';
|
|
191
|
+
export type {
|
|
192
|
+
AIComposerConfig,
|
|
193
|
+
AIComposerOptions,
|
|
194
|
+
SubmitConfig,
|
|
195
|
+
SubmitKey,
|
|
196
|
+
} from './types';
|
|
197
|
+
|
|
198
|
+
// errors
|
|
199
|
+
export { ErrorCode, AIComposerError, toError, type AIComposerErrorCode } from './errors';
|
|
200
|
+
|
|
201
|
+
export { escapeHtml } from './utils/html';
|