@critical-path/core 0.9.0 → 0.11.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/CHANGELOG.md +18 -0
- package/README.md +2 -2
- package/dist/domain/events.d.ts +21 -2
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js.map +1 -1
- package/dist/engine/index.d.ts +8 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +76 -2
- package/dist/engine/index.js.map +1 -1
- package/dist/index.test.js +71 -0
- package/dist/index.test.js.map +1 -1
- package/dist/store/firebase.d.ts +8 -0
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +22 -0
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/index.d.ts +16 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +34 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +9 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +59 -12
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sqlite.test.js +14 -1
- package/dist/store/sqlite.test.js.map +1 -1
- package/dist/types/index.d.ts +8 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/mentions.d.ts +24 -0
- package/dist/utils/mentions.d.ts.map +1 -0
- package/dist/utils/mentions.js +73 -0
- package/dist/utils/mentions.js.map +1 -0
- package/dist/utils/mentions.test.d.ts +2 -0
- package/dist/utils/mentions.test.d.ts.map +1 -0
- package/dist/utils/mentions.test.js +71 -0
- package/dist/utils/mentions.test.js.map +1 -0
- package/package.json +1 -1
- package/src/domain/events.ts +13 -0
- package/src/engine/index.ts +85 -2
- package/src/index.test.ts +83 -0
- package/src/store/firebase.ts +24 -0
- package/src/store/index.ts +38 -0
- package/src/store/sqlite.test.ts +17 -1
- package/src/store/sqlite.ts +64 -10
- package/src/types/index.ts +10 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/mentions.test.ts +82 -0
- package/src/utils/mentions.ts +89 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/store/index.ts
CHANGED
|
@@ -71,6 +71,8 @@ export interface CommentRepository {
|
|
|
71
71
|
addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment>;
|
|
72
72
|
updateComment(id: string, updates: Partial<Comment>): Promise<Comment | null>;
|
|
73
73
|
deleteComment(id: string): Promise<boolean>;
|
|
74
|
+
addReaction?(commentId: string, reaction: { emoji: string; userId: string }): Promise<Comment | null>;
|
|
75
|
+
removeReaction?(commentId: string, reaction: { emoji: string; userId: string }): Promise<Comment | null>;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
export interface AttachmentRepository {
|
|
@@ -369,6 +371,42 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
369
371
|
return this.comments.delete(id);
|
|
370
372
|
}
|
|
371
373
|
|
|
374
|
+
async addReaction(commentId: string, reaction: { emoji: string; userId: string }): Promise<Comment | null> {
|
|
375
|
+
const existing = this.comments.get(commentId);
|
|
376
|
+
if (!existing) return null;
|
|
377
|
+
const reactions = existing.reactions ? [...existing.reactions] : [];
|
|
378
|
+
const alreadyExists = reactions.some((r) => r.emoji === reaction.emoji && r.userId === reaction.userId);
|
|
379
|
+
if (!alreadyExists) {
|
|
380
|
+
reactions.push({
|
|
381
|
+
emoji: reaction.emoji,
|
|
382
|
+
userId: reaction.userId,
|
|
383
|
+
createdAt: new Date().toISOString()
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const updated: Comment = {
|
|
387
|
+
...existing,
|
|
388
|
+
reactions,
|
|
389
|
+
updatedAt: new Date().toISOString()
|
|
390
|
+
};
|
|
391
|
+
this.comments.set(commentId, updated);
|
|
392
|
+
return updated;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async removeReaction(commentId: string, reaction: { emoji: string; userId: string }): Promise<Comment | null> {
|
|
396
|
+
const existing = this.comments.get(commentId);
|
|
397
|
+
if (!existing) return null;
|
|
398
|
+
const reactions = (existing.reactions || []).filter(
|
|
399
|
+
(r) => !(r.emoji === reaction.emoji && r.userId === reaction.userId)
|
|
400
|
+
);
|
|
401
|
+
const updated: Comment = {
|
|
402
|
+
...existing,
|
|
403
|
+
reactions,
|
|
404
|
+
updatedAt: new Date().toISOString()
|
|
405
|
+
};
|
|
406
|
+
this.comments.set(commentId, updated);
|
|
407
|
+
return updated;
|
|
408
|
+
}
|
|
409
|
+
|
|
372
410
|
// Attachments
|
|
373
411
|
async getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }): Promise<Attachment[]> {
|
|
374
412
|
return Array.from(this.attachments.values()).filter((a) => {
|
package/src/store/sqlite.test.ts
CHANGED
|
@@ -77,11 +77,16 @@ describe('SQLiteStore', () => {
|
|
|
77
77
|
const comments = await store.getComments('task_1');
|
|
78
78
|
expect(comments).toHaveLength(2);
|
|
79
79
|
|
|
80
|
-
const updatedComment = await store.updateComment(comment.id, {
|
|
80
|
+
const updatedComment = await store.updateComment(comment.id, {
|
|
81
|
+
content: 'Updated content',
|
|
82
|
+
mentions: ['planner', 'coordinator']
|
|
83
|
+
});
|
|
81
84
|
expect(updatedComment?.content).toBe('Updated content');
|
|
85
|
+
expect(updatedComment?.mentions).toEqual(['planner', 'coordinator']);
|
|
82
86
|
|
|
83
87
|
const fetchedComment = await store.getComment(comment.id);
|
|
84
88
|
expect(fetchedComment?.content).toBe('Updated content');
|
|
89
|
+
expect(fetchedComment?.mentions).toEqual(['planner', 'coordinator']);
|
|
85
90
|
|
|
86
91
|
const timeEntry = await store.logTime({
|
|
87
92
|
taskId: 'task_1',
|
|
@@ -94,6 +99,17 @@ describe('SQLiteStore', () => {
|
|
|
94
99
|
const entries = await store.getTimeEntries('task_1');
|
|
95
100
|
expect(entries).toHaveLength(1);
|
|
96
101
|
|
|
102
|
+
// Emoji reactions
|
|
103
|
+
const reactComment = await store.addReaction(comment.id, { emoji: '🔥', userId: 'user_2' });
|
|
104
|
+
expect(reactComment?.reactions).toHaveLength(1);
|
|
105
|
+
expect(reactComment?.reactions?.[0].emoji).toBe('🔥');
|
|
106
|
+
|
|
107
|
+
const fetchedWithReactions = await store.getComment(comment.id);
|
|
108
|
+
expect(fetchedWithReactions?.reactions).toHaveLength(1);
|
|
109
|
+
|
|
110
|
+
const unreactComment = await store.removeReaction(comment.id, { emoji: '🔥', userId: 'user_2' });
|
|
111
|
+
expect(unreactComment?.reactions).toHaveLength(0);
|
|
112
|
+
|
|
97
113
|
const deletedComment = await store.deleteComment(reply.id);
|
|
98
114
|
expect(deletedComment).toBe(true);
|
|
99
115
|
expect(await store.getComments('task_1')).toHaveLength(1);
|
package/src/store/sqlite.ts
CHANGED
|
@@ -154,6 +154,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
154
154
|
authorType TEXT,
|
|
155
155
|
parentId TEXT,
|
|
156
156
|
content TEXT NOT NULL,
|
|
157
|
+
mentions TEXT,
|
|
158
|
+
reactions TEXT,
|
|
157
159
|
metadata TEXT,
|
|
158
160
|
createdAt TEXT NOT NULL,
|
|
159
161
|
updatedAt TEXT NOT NULL
|
|
@@ -212,6 +214,18 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
212
214
|
createdAt TEXT NOT NULL
|
|
213
215
|
);
|
|
214
216
|
`);
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
this.db.exec('ALTER TABLE comments ADD COLUMN reactions TEXT');
|
|
220
|
+
} catch {
|
|
221
|
+
// Column may already exist
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
this.db.exec('ALTER TABLE comments ADD COLUMN mentions TEXT');
|
|
226
|
+
} catch {
|
|
227
|
+
// Column may already exist
|
|
228
|
+
}
|
|
215
229
|
}
|
|
216
230
|
|
|
217
231
|
// --- Projects ---
|
|
@@ -682,23 +696,33 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
682
696
|
}
|
|
683
697
|
|
|
684
698
|
// --- Comments ---
|
|
699
|
+
private mapComment(row: any): Comment {
|
|
700
|
+
return {
|
|
701
|
+
id: row.id,
|
|
702
|
+
taskId: row.taskId,
|
|
703
|
+
authorId: row.authorId,
|
|
704
|
+
authorType: row.authorType,
|
|
705
|
+
parentId: row.parentId || undefined,
|
|
706
|
+
content: row.content,
|
|
707
|
+
mentions: row.mentions ? JSON.parse(row.mentions) : undefined,
|
|
708
|
+
reactions: row.reactions ? JSON.parse(row.reactions) : undefined,
|
|
709
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
710
|
+
createdAt: row.createdAt,
|
|
711
|
+
updatedAt: row.updatedAt
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
685
715
|
async getComments(taskId: string): Promise<Comment[]> {
|
|
686
716
|
const stmt = this.db.prepare('SELECT * FROM comments WHERE taskId = ? ORDER BY createdAt ASC');
|
|
687
717
|
const rows = stmt.all(taskId) as any[];
|
|
688
|
-
return rows.map((r) => (
|
|
689
|
-
...r,
|
|
690
|
-
metadata: r.metadata ? JSON.parse(r.metadata) : undefined
|
|
691
|
-
}));
|
|
718
|
+
return rows.map((r) => this.mapComment(r));
|
|
692
719
|
}
|
|
693
720
|
|
|
694
721
|
async getComment(id: string): Promise<Comment | null> {
|
|
695
722
|
const stmt = this.db.prepare('SELECT * FROM comments WHERE id = ?');
|
|
696
723
|
const row = stmt.get(id) as any;
|
|
697
724
|
if (!row) return null;
|
|
698
|
-
return
|
|
699
|
-
...row,
|
|
700
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : undefined
|
|
701
|
-
};
|
|
725
|
+
return this.mapComment(row);
|
|
702
726
|
}
|
|
703
727
|
|
|
704
728
|
async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
|
|
@@ -707,8 +731,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
707
731
|
const newComment: Comment = { ...comment, id, createdAt: now, updatedAt: now };
|
|
708
732
|
|
|
709
733
|
const stmt = this.db.prepare(`
|
|
710
|
-
INSERT INTO comments (id, taskId, authorId, authorType, parentId, content, metadata, createdAt, updatedAt)
|
|
711
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
734
|
+
INSERT INTO comments (id, taskId, authorId, authorType, parentId, content, mentions, reactions, metadata, createdAt, updatedAt)
|
|
735
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
712
736
|
`);
|
|
713
737
|
stmt.run(
|
|
714
738
|
newComment.id,
|
|
@@ -717,6 +741,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
717
741
|
newComment.authorType || 'user',
|
|
718
742
|
newComment.parentId || null,
|
|
719
743
|
newComment.content,
|
|
744
|
+
newComment.mentions ? JSON.stringify(newComment.mentions) : null,
|
|
745
|
+
newComment.reactions ? JSON.stringify(newComment.reactions) : null,
|
|
720
746
|
newComment.metadata ? JSON.stringify(newComment.metadata) : null,
|
|
721
747
|
newComment.createdAt,
|
|
722
748
|
newComment.updatedAt
|
|
@@ -740,6 +766,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
740
766
|
authorType = ?,
|
|
741
767
|
parentId = ?,
|
|
742
768
|
content = ?,
|
|
769
|
+
mentions = ?,
|
|
770
|
+
reactions = ?,
|
|
743
771
|
metadata = ?,
|
|
744
772
|
updatedAt = ?
|
|
745
773
|
WHERE id = ?
|
|
@@ -749,6 +777,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
749
777
|
updated.authorType || 'user',
|
|
750
778
|
updated.parentId || null,
|
|
751
779
|
updated.content,
|
|
780
|
+
updated.mentions ? JSON.stringify(updated.mentions) : null,
|
|
781
|
+
updated.reactions ? JSON.stringify(updated.reactions) : null,
|
|
752
782
|
updated.metadata ? JSON.stringify(updated.metadata) : null,
|
|
753
783
|
updated.updatedAt,
|
|
754
784
|
id
|
|
@@ -762,6 +792,30 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
762
792
|
return (result.changes ?? 0) > 0;
|
|
763
793
|
}
|
|
764
794
|
|
|
795
|
+
async addReaction(commentId: string, reaction: { emoji: string; userId: string }): Promise<Comment | null> {
|
|
796
|
+
const existing = await this.getComment(commentId);
|
|
797
|
+
if (!existing) return null;
|
|
798
|
+
const reactions = existing.reactions ? [...existing.reactions] : [];
|
|
799
|
+
const alreadyExists = reactions.some((r) => r.emoji === reaction.emoji && r.userId === reaction.userId);
|
|
800
|
+
if (!alreadyExists) {
|
|
801
|
+
reactions.push({
|
|
802
|
+
emoji: reaction.emoji,
|
|
803
|
+
userId: reaction.userId,
|
|
804
|
+
createdAt: new Date().toISOString()
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return this.updateComment(commentId, { reactions });
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async removeReaction(commentId: string, reaction: { emoji: string; userId: string }): Promise<Comment | null> {
|
|
811
|
+
const existing = await this.getComment(commentId);
|
|
812
|
+
if (!existing) return null;
|
|
813
|
+
const reactions = (existing.reactions || []).filter(
|
|
814
|
+
(r) => !(r.emoji === reaction.emoji && r.userId === reaction.userId)
|
|
815
|
+
);
|
|
816
|
+
return this.updateComment(commentId, { reactions });
|
|
817
|
+
}
|
|
818
|
+
|
|
765
819
|
// --- Attachments ---
|
|
766
820
|
async getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }): Promise<Attachment[]> {
|
|
767
821
|
let sql = 'SELECT * FROM attachments';
|
package/src/types/index.ts
CHANGED
|
@@ -185,6 +185,12 @@ export interface Iteration {
|
|
|
185
185
|
|
|
186
186
|
export type AuthorType = 'user' | 'agent' | 'system';
|
|
187
187
|
|
|
188
|
+
export interface CommentReaction {
|
|
189
|
+
emoji: string;
|
|
190
|
+
userId: string;
|
|
191
|
+
createdAt?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
188
194
|
export interface Comment {
|
|
189
195
|
id: string;
|
|
190
196
|
taskId: string;
|
|
@@ -192,6 +198,8 @@ export interface Comment {
|
|
|
192
198
|
authorType?: AuthorType;
|
|
193
199
|
parentId?: string; // Threaded reply support
|
|
194
200
|
content: string;
|
|
201
|
+
mentions?: string[];
|
|
202
|
+
reactions?: CommentReaction[];
|
|
195
203
|
metadata?: Record<string, unknown>;
|
|
196
204
|
createdAt: string;
|
|
197
205
|
updatedAt: string;
|
|
@@ -291,6 +299,8 @@ export type WebhookEvent =
|
|
|
291
299
|
| 'comment.created'
|
|
292
300
|
| 'comment.updated'
|
|
293
301
|
| 'comment.deleted'
|
|
302
|
+
| 'comment.reaction.added'
|
|
303
|
+
| 'comment.reaction.removed'
|
|
294
304
|
| 'attachment.created'
|
|
295
305
|
| 'attachment.deleted'
|
|
296
306
|
| 'iteration.started'
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { extractMentions, parseMentionSegments } from './mentions.js';
|
|
3
|
+
|
|
4
|
+
describe('mentions utility', () => {
|
|
5
|
+
describe('extractMentions', () => {
|
|
6
|
+
it('returns empty array for empty or non-string input', () => {
|
|
7
|
+
expect(extractMentions('')).toEqual([]);
|
|
8
|
+
expect(extractMentions(null as any)).toEqual([]);
|
|
9
|
+
expect(extractMentions(undefined as any)).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('extracts single mention', () => {
|
|
13
|
+
expect(extractMentions('Hello @planner, please review.')).toEqual(['planner']);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('extracts multiple mentions and deduplicates them', () => {
|
|
17
|
+
const text = 'Hey @jane and @planner, can @jane check with @coordinator?';
|
|
18
|
+
expect(extractMentions(text)).toEqual(['jane', 'planner', 'coordinator']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('supports quoted handles with spaces', () => {
|
|
22
|
+
const text = 'Assigning to @"Agent Planner" and @"Jane Doe"';
|
|
23
|
+
expect(extractMentions(text)).toEqual(['Agent Planner', 'Jane Doe']);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('ignores email addresses without leading space or boundary', () => {
|
|
27
|
+
const text = 'Send mail to user@example.com or ping @alex';
|
|
28
|
+
expect(extractMentions(text)).toEqual(['alex']);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('handles mentions with dashes, underscores, and periods', () => {
|
|
32
|
+
const text = 'Call @agent-1, @super_user, and @john.doe';
|
|
33
|
+
expect(extractMentions(text)).toEqual(['agent-1', 'super_user', 'john.doe']);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('parseMentionSegments', () => {
|
|
38
|
+
it('returns empty array for empty string', () => {
|
|
39
|
+
expect(parseMentionSegments('')).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns single text segment if no mentions', () => {
|
|
43
|
+
expect(parseMentionSegments('Just a normal comment without tags.')).toEqual([
|
|
44
|
+
{ type: 'text', value: 'Just a normal comment without tags.' }
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('correctly segments text with single mention in the middle', () => {
|
|
49
|
+
const result = parseMentionSegments('Hello @planner, please check.');
|
|
50
|
+
expect(result).toEqual([
|
|
51
|
+
{ type: 'text', value: 'Hello ' },
|
|
52
|
+
{ type: 'mention', value: '@planner', handle: 'planner' },
|
|
53
|
+
{ type: 'text', value: ', please check.' }
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('correctly segments text starting with a mention', () => {
|
|
58
|
+
const result = parseMentionSegments('@planner can you take a look?');
|
|
59
|
+
expect(result).toEqual([
|
|
60
|
+
{ type: 'mention', value: '@planner', handle: 'planner' },
|
|
61
|
+
{ type: 'text', value: ' can you take a look?' }
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('correctly segments text ending with a mention', () => {
|
|
66
|
+
const result = parseMentionSegments('Assigned to @coordinator');
|
|
67
|
+
expect(result).toEqual([
|
|
68
|
+
{ type: 'text', value: 'Assigned to ' },
|
|
69
|
+
{ type: 'mention', value: '@coordinator', handle: 'coordinator' }
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('correctly segments quoted mentions', () => {
|
|
74
|
+
const result = parseMentionSegments('Ping @"Agent Supervisor" ASAP');
|
|
75
|
+
expect(result).toEqual([
|
|
76
|
+
{ type: 'text', value: 'Ping ' },
|
|
77
|
+
{ type: 'mention', value: '@"Agent Supervisor"', handle: 'Agent Supervisor' },
|
|
78
|
+
{ type: 'text', value: ' ASAP' }
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface MentionSegment {
|
|
2
|
+
type: 'text' | 'mention';
|
|
3
|
+
value: string;
|
|
4
|
+
handle?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Regex matching mentions:
|
|
9
|
+
* - Quoted mentions: @"Jane Doe"
|
|
10
|
+
* - Standard mentions: @jane_doe, @planner, @user-1, @john.smith
|
|
11
|
+
* Note: Does not match emails like foo@bar.com (requires word boundary or start of string before @).
|
|
12
|
+
*/
|
|
13
|
+
export const MENTION_REGEX = /(?:^|\s)@(?:"([^"]+)"|([a-zA-Z0-9_.-]+))/g;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extracts unique mention handles from a text string.
|
|
17
|
+
* Strips quotes and the leading '@' symbol.
|
|
18
|
+
* Preserves the order of their first appearance.
|
|
19
|
+
*/
|
|
20
|
+
export function extractMentions(content: string): string[] {
|
|
21
|
+
if (!content || typeof content !== 'string') return [];
|
|
22
|
+
|
|
23
|
+
const mentions: string[] = [];
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
|
|
26
|
+
// Reset regex lastIndex
|
|
27
|
+
const regex = new RegExp(MENTION_REGEX.source, 'g');
|
|
28
|
+
let match: RegExpExecArray | null;
|
|
29
|
+
|
|
30
|
+
while ((match = regex.exec(content)) !== null) {
|
|
31
|
+
const handle = match[1] ?? match[2];
|
|
32
|
+
if (handle && !seen.has(handle)) {
|
|
33
|
+
seen.add(handle);
|
|
34
|
+
mentions.push(handle);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return mentions;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Segments a text string into plain text parts and mention tokens.
|
|
43
|
+
* Useful for rendering highlighted mention badges or links in UI components.
|
|
44
|
+
*/
|
|
45
|
+
export function parseMentionSegments(content: string): MentionSegment[] {
|
|
46
|
+
if (!content) return [];
|
|
47
|
+
|
|
48
|
+
const segments: MentionSegment[] = [];
|
|
49
|
+
const regex = new RegExp(MENTION_REGEX.source, 'g');
|
|
50
|
+
let lastIndex = 0;
|
|
51
|
+
let match: RegExpExecArray | null;
|
|
52
|
+
|
|
53
|
+
while ((match = regex.exec(content)) !== null) {
|
|
54
|
+
const fullMatch = match[0];
|
|
55
|
+
const leadingWhitespace = fullMatch.startsWith(' ') || fullMatch.startsWith('\t') || fullMatch.startsWith('\n')
|
|
56
|
+
? fullMatch[0]
|
|
57
|
+
: '';
|
|
58
|
+
const mentionToken = fullMatch.slice(leadingWhitespace.length);
|
|
59
|
+
const handle = match[1] ?? match[2];
|
|
60
|
+
const matchStart = match.index + leadingWhitespace.length;
|
|
61
|
+
|
|
62
|
+
// Push text before this mention if any
|
|
63
|
+
if (matchStart > lastIndex) {
|
|
64
|
+
segments.push({
|
|
65
|
+
type: 'text',
|
|
66
|
+
value: content.slice(lastIndex, matchStart)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Push the mention token
|
|
71
|
+
segments.push({
|
|
72
|
+
type: 'mention',
|
|
73
|
+
value: mentionToken,
|
|
74
|
+
handle
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
lastIndex = match.index + fullMatch.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Push any remaining text after the last match
|
|
81
|
+
if (lastIndex < content.length) {
|
|
82
|
+
segments.push({
|
|
83
|
+
type: 'text',
|
|
84
|
+
value: content.slice(lastIndex)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return segments;
|
|
89
|
+
}
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/index.test.ts","./src/index.ts","./src/use-case-validation.test.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/index.test.ts","./src/index.ts","./src/use-case-validation.test.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|