@adecore/agents-react 0.37.1 → 0.37.3
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/dist/chat/chat-client.d.ts +1 -0
- package/dist/chat/chat-client.js +2 -1
- package/dist/chat/chat-references.d.ts +16 -1
- package/dist/chat/chat-references.js +29 -3
- package/dist/chat/drafts.d.ts +2 -0
- package/dist/chat/drafts.js +21 -5
- package/dist/chat/index.d.ts +1 -1
- package/dist/chat/logic/picker-entries.d.ts +6 -1
- package/dist/chat/logic/picker-entries.js +10 -0
- package/dist/chat/mentions.d.ts +31 -14
- package/dist/chat/mentions.js +186 -41
- package/dist/chat/stash.d.ts +1 -0
- package/dist/chat/stash.js +4 -2
- package/dist/chat/ui/Composer.js +438 -429
- package/dist/chat/ui/ComposerInput.d.ts +1 -0
- package/dist/chat/ui/ComposerInput.js +2 -0
- package/dist/chat/ui/Markdown.d.ts +3 -1
- package/dist/chat/ui/Markdown.js +194 -120
- package/dist/chat/ui/MessageActions.js +70 -66
- package/dist/chat/ui/Pickers.js +151 -207
- package/dist/chat/ui/ReferenceChip.d.ts +15 -0
- package/dist/chat/ui/ReferenceChip.js +255 -0
- package/dist/chat/ui/RunSettings.js +25 -21
- package/dist/chat/ui/Timeline.js +52 -29
- package/dist/chat/ui/TimelineMenu.js +84 -78
- package/dist/chat/ui/chip-clipboard.d.ts +9 -0
- package/dist/chat/ui/chip-clipboard.js +105 -0
- package/dist/chat/ui/chips.d.ts +5 -0
- package/dist/chat/ui/chips.js +15 -1
- package/dist/chat/ui/composer/ComposerMenu.js +140 -79
- package/dist/chat/ui/composer/DraftParts.d.ts +0 -4
- package/dist/chat/ui/composer/DraftParts.js +0 -41
- package/dist/chat/ui/composer/chip-paste.d.ts +15 -0
- package/dist/chat/ui/composer/chip-paste.js +104 -0
- package/dist/chat/ui/composer/chips.d.ts +5 -0
- package/dist/chat/ui/composer/chips.js +59 -9
- package/dist/chat/ui/composer/key-handler.js +9 -0
- package/dist/chat/ui/composer/use-composer-draft.d.ts +0 -1
- package/dist/chat/ui/composer/use-composer-draft.js +2 -10
- package/dist/chat/ui/composer/use-composer-menus.d.ts +12 -2
- package/dist/chat/ui/composer/use-composer-menus.js +192 -149
- package/dist/chat/ui/composer/use-composer-submit.js +17 -12
- package/dist/chat/ui/composer/use-draft-files.d.ts +7 -1
- package/dist/chat/ui/composer/use-draft-files.js +90 -58
- package/dist/chat/ui/composer/use-take-back.js +2 -1
- package/dist/chat/ui/index.d.ts +0 -1
- package/dist/chat/ui/index.js +0 -1
- package/dist/chat/ui/reference-icons.d.ts +3 -0
- package/dist/chat/ui/reference-icons.js +15 -0
- package/dist/chat/ui/reference-lookup.d.ts +7 -0
- package/dist/chat/ui/reference-lookup.js +6 -0
- package/dist/chat/ui/rehype-chips.d.ts +8 -1
- package/dist/chat/ui/rehype-chips.js +35 -11
- package/dist/chat/ui/rows/MessageRows.js +218 -158
- package/dist/chat/ui/thread-references.d.ts +4 -0
- package/dist/chat/ui/thread-references.js +78 -0
- package/dist/host-hooks.d.ts +2 -0
- package/dist/host-hooks.js +7 -0
- package/dist/host.d.ts +3 -0
- package/dist/locales/en/agent-chat.d.ts +20 -3
- package/dist/locales/en/agent-chat.js +24 -7
- package/dist/locales/nl/agent-chat.d.ts +20 -3
- package/dist/locales/nl/agent-chat.js +24 -7
- package/package.json +8 -8
- package/src/chat/chat-client.ts +4 -1
- package/src/chat/chat-references.ts +44 -3
- package/src/chat/drafts.ts +24 -6
- package/src/chat/index.ts +1 -1
- package/src/chat/logic/picker-entries.ts +12 -1
- package/src/chat/mentions.ts +202 -36
- package/src/chat/stash.ts +6 -2
- package/src/chat/ui/Composer.tsx +14 -4
- package/src/chat/ui/ComposerInput.tsx +4 -0
- package/src/chat/ui/Markdown.tsx +57 -25
- package/src/chat/ui/MessageActions.tsx +5 -2
- package/src/chat/ui/Pickers.tsx +8 -2
- package/src/chat/ui/ReferenceChip.tsx +74 -0
- package/src/chat/ui/RunSettings.tsx +29 -23
- package/src/chat/ui/Timeline.tsx +24 -2
- package/src/chat/ui/TimelineMenu.tsx +7 -3
- package/src/chat/ui/chip-clipboard.ts +115 -0
- package/src/chat/ui/chips.ts +20 -1
- package/src/chat/ui/composer/ComposerMenu.tsx +53 -37
- package/src/chat/ui/composer/DraftParts.tsx +0 -12
- package/src/chat/ui/composer/chip-paste.ts +122 -0
- package/src/chat/ui/composer/chips.ts +60 -10
- package/src/chat/ui/composer/key-handler.ts +9 -0
- package/src/chat/ui/composer/use-composer-draft.ts +1 -7
- package/src/chat/ui/composer/use-composer-menus.ts +68 -29
- package/src/chat/ui/composer/use-composer-submit.ts +13 -6
- package/src/chat/ui/composer/use-draft-files.ts +39 -5
- package/src/chat/ui/composer/use-take-back.ts +2 -1
- package/src/chat/ui/index.ts +0 -1
- package/src/chat/ui/reference-icons.ts +17 -0
- package/src/chat/ui/reference-lookup.ts +13 -0
- package/src/chat/ui/rehype-chips.ts +38 -13
- package/src/chat/ui/rows/MessageRows.tsx +88 -64
- package/src/chat/ui/thread-references.ts +47 -0
- package/src/host-hooks.ts +9 -0
- package/src/host.ts +6 -1
- package/src/locales/en/agent-chat.ts +24 -7
- package/src/locales/nl/agent-chat.ts +24 -7
- package/dist/chat/ui/ChatReferenceChip.d.ts +0 -4
- package/dist/chat/ui/ChatReferenceChip.js +0 -110
- package/src/chat/ui/ChatReferenceChip.tsx +0 -26
package/dist/chat/chat-client.js
CHANGED
|
@@ -184,7 +184,8 @@ export class ChatClient {
|
|
|
184
184
|
skills: extras.skills,
|
|
185
185
|
chats: extras.chats,
|
|
186
186
|
quoteSource: extras.quoteSource,
|
|
187
|
-
attachments: extras.attachments
|
|
187
|
+
attachments: extras.attachments,
|
|
188
|
+
references: extras.references
|
|
188
189
|
});
|
|
189
190
|
}
|
|
190
191
|
/* Marks a message; the list every client of the chat holds comes back as `chat.bookmarks`. */
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
import type { ReferenceToken } from './mentions.js';
|
|
1
2
|
export interface ProjectChat {
|
|
2
3
|
id: string;
|
|
3
4
|
title: string;
|
|
4
5
|
}
|
|
5
|
-
export
|
|
6
|
+
export type ReferableKind = 'canvas' | 'chat' | 'terminal' | 'note' | 'text' | 'drawing' | 'diagram' | 'browser' | 'device' | 'file' | 'database';
|
|
7
|
+
export interface Referable {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
kind: ReferableKind;
|
|
11
|
+
detail?: string;
|
|
12
|
+
group: 'canvas' | 'views';
|
|
13
|
+
}
|
|
14
|
+
export interface Referables {
|
|
15
|
+
items: readonly Referable[];
|
|
16
|
+
chatsOnly: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function referablesOfChats(chats: readonly ProjectChat[]): Referables;
|
|
19
|
+
export declare function referenceTokens(ids: readonly string[], items: readonly Referable[], spelled?: Readonly<Record<string, string>>): ReferenceToken[];
|
|
20
|
+
export declare function referenceSuggestions(referables: Referables, query: string, exclude: readonly string[], limit: number): Referable[];
|
|
@@ -1,5 +1,31 @@
|
|
|
1
|
-
/*
|
|
2
|
-
export function
|
|
1
|
+
/* What a host without views offers: its chats, which every machine takes. */
|
|
2
|
+
export function referablesOfChats(chats) {
|
|
3
|
+
return {
|
|
4
|
+
items: chats.map(chat => ({
|
|
5
|
+
id: chat.id,
|
|
6
|
+
name: chat.title,
|
|
7
|
+
kind: 'chat',
|
|
8
|
+
group: 'views'
|
|
9
|
+
})),
|
|
10
|
+
chatsOnly: true
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/*
|
|
14
|
+
* How each id is spelled in a text: under the name it was picked with where `spelled` knows it, else
|
|
15
|
+
* under the name it has now. An id with neither has no token.
|
|
16
|
+
*/
|
|
17
|
+
export function referenceTokens(ids, items, spelled = {}) {
|
|
18
|
+
return [...new Set(ids)].flatMap(id => {
|
|
19
|
+
const name = spelled[id] ?? items.find(item => item.id === id)?.name;
|
|
20
|
+
return name === undefined || name === '' ? [] : [{
|
|
21
|
+
id,
|
|
22
|
+
name
|
|
23
|
+
}];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/* What a `#query` offers, the canvas first: never an id in `exclude`, and only chats where the machine takes no more. */
|
|
27
|
+
export function referenceSuggestions(referables, query, exclude, limit) {
|
|
3
28
|
const needle = query.toLowerCase();
|
|
4
|
-
|
|
29
|
+
const matches = referables.items.filter(item => item.name !== '' && (!referables.chatsOnly || item.kind === 'chat') && !exclude.includes(item.id) && item.name.toLowerCase().includes(needle));
|
|
30
|
+
return [...matches.filter(item => item.group === 'canvas'), ...matches.filter(item => item.group === 'views')].slice(0, limit);
|
|
5
31
|
}
|
package/dist/chat/drafts.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface ChatDraft {
|
|
|
7
7
|
attachments: ChatAttachmentUpload[];
|
|
8
8
|
quote: string;
|
|
9
9
|
quoteSource: string | null;
|
|
10
|
+
references: string[];
|
|
11
|
+
referenceNames?: Record<string, string>;
|
|
10
12
|
}
|
|
11
13
|
export declare const EMPTY_DRAFT: ChatDraft;
|
|
12
14
|
export declare function isEmptyDraft(draft: ChatDraft): boolean;
|
package/dist/chat/drafts.js
CHANGED
|
@@ -10,7 +10,8 @@ export const EMPTY_DRAFT = {
|
|
|
10
10
|
chats: [],
|
|
11
11
|
attachments: [],
|
|
12
12
|
quote: '',
|
|
13
|
-
quoteSource: null
|
|
13
|
+
quoteSource: null,
|
|
14
|
+
references: []
|
|
14
15
|
};
|
|
15
16
|
const storage = persistedJson(storageKey, raw => raw ? JSON.parse(raw) : {}, {}, () => chatStorageLegacyKeys('drafts'));
|
|
16
17
|
export function isEmptyDraft(draft) {
|
|
@@ -58,7 +59,11 @@ export function readDraft(chatId) {
|
|
|
58
59
|
chats: record.chats ?? [],
|
|
59
60
|
attachments: record.attachments ?? [],
|
|
60
61
|
quote: record.quote ?? '',
|
|
61
|
-
quoteSource: record.quoteSource ?? null
|
|
62
|
+
quoteSource: record.quoteSource ?? null,
|
|
63
|
+
references: record.references ?? [],
|
|
64
|
+
...(record.referenceNames === undefined ? {} : {
|
|
65
|
+
referenceNames: record.referenceNames
|
|
66
|
+
})
|
|
62
67
|
} : EMPTY_DRAFT;
|
|
63
68
|
}
|
|
64
69
|
export function writeDraft(chatId, draft) {
|
|
@@ -78,7 +83,9 @@ export function writeDraft(chatId, draft) {
|
|
|
78
83
|
quote: draft.quote,
|
|
79
84
|
...(draft.quoteSource === null ? {} : {
|
|
80
85
|
quoteSource: draft.quoteSource
|
|
81
|
-
})
|
|
86
|
+
}),
|
|
87
|
+
references: draft.references,
|
|
88
|
+
...namesOf(draft)
|
|
82
89
|
};
|
|
83
90
|
if (storage.write(drafts)) {
|
|
84
91
|
return;
|
|
@@ -92,10 +99,17 @@ export function writeDraft(chatId, draft) {
|
|
|
92
99
|
quote: draft.quote,
|
|
93
100
|
...(draft.quoteSource === null ? {} : {
|
|
94
101
|
quoteSource: draft.quoteSource
|
|
95
|
-
})
|
|
102
|
+
}),
|
|
103
|
+
references: draft.references,
|
|
104
|
+
...namesOf(draft)
|
|
96
105
|
};
|
|
97
106
|
storage.write(drafts);
|
|
98
107
|
}
|
|
108
|
+
function namesOf(draft) {
|
|
109
|
+
return draft.referenceNames === undefined ? {} : {
|
|
110
|
+
referenceNames: draft.referenceNames
|
|
111
|
+
};
|
|
112
|
+
}
|
|
99
113
|
/* Text handed to a draft from outside the composer goes under what was already typed, never over it. */
|
|
100
114
|
export function joinDraftText(current, added) {
|
|
101
115
|
return current.trim() === '' ? added : `${current.replace(/\s+$/, '')}\n\n${added}`;
|
|
@@ -123,7 +137,9 @@ export function takeBackIntoDraft(current, taken) {
|
|
|
123
137
|
chats: union(taken.chats, current.chats),
|
|
124
138
|
attachments: [...checked.accepted.map(entry => entry.upload), ...current.attachments],
|
|
125
139
|
quote: current.quote,
|
|
126
|
-
quoteSource: current.quoteSource
|
|
140
|
+
quoteSource: current.quoteSource,
|
|
141
|
+
references: union(taken.references, current.references),
|
|
142
|
+
...namesOf(current)
|
|
127
143
|
},
|
|
128
144
|
rejected: checked.rejected
|
|
129
145
|
};
|
package/dist/chat/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { type AccountChoice, useAccountChoice, type NewAccountChoice, useNewAccountChoice } from './account-choice.js';
|
|
2
2
|
export { transportActions, actionsOf, useChatActions } from './actions.js';
|
|
3
3
|
export { type ChatSendExtras, ChatClient } from './chat-client.js';
|
|
4
|
-
export type { ProjectChat } from './chat-references.js';
|
|
4
|
+
export type { ProjectChat, Referable, ReferableKind, Referables } from './chat-references.js';
|
|
5
5
|
export { sendFromComposer, type ComposerSend } from './composer-send.js';
|
|
6
6
|
export { stepEffortFromComposer } from './composer-effort.js';
|
|
7
7
|
export type { EffortStep } from './logic/effort.js';
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentKind, type ModelInfo, type ProviderInfo } from '@adecore/agent-contracts';
|
|
2
|
+
export declare function inPickerOrder<T extends {
|
|
3
|
+
kind: AgentKind;
|
|
4
|
+
name: string;
|
|
5
|
+
}>(providers: readonly T[]): T[];
|
|
6
|
+
export declare function separatesGroups(previous: AgentKind | undefined, kind: AgentKind): boolean;
|
|
2
7
|
export declare const COMPACT_MODEL_LIMIT = 8;
|
|
3
8
|
export type PickerEntry = {
|
|
4
9
|
kind: 'model';
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import { pickerGroupsOf, providerGroupOf } from '@adecore/agent-contracts';
|
|
1
2
|
import { nameCollator } from '@adecore/ui/format';
|
|
3
|
+
/* The providers in the order a picker lists them: the frontier labs, the rest, then the API providers, each by name. */
|
|
4
|
+
export function inPickerOrder(providers) {
|
|
5
|
+
const groups = pickerGroupsOf(providers);
|
|
6
|
+
return [...groups.frontier, ...groups.others, ...groups.api];
|
|
7
|
+
}
|
|
8
|
+
/* Whether a separator goes between two providers next to each other in that order: where one group ends. */
|
|
9
|
+
export function separatesGroups(previous, kind) {
|
|
10
|
+
return previous !== undefined && providerGroupOf(previous) !== providerGroupOf(kind);
|
|
11
|
+
}
|
|
2
12
|
/*
|
|
3
13
|
* A provider with more live models than this keeps the list short: the model that is picked and the default,
|
|
4
14
|
* with the rest behind one row that opens all of them. A list of three hundred models is not one to scroll.
|
package/dist/chat/mentions.d.ts
CHANGED
|
@@ -4,10 +4,25 @@ export declare const MENTION_DRAG_TYPES: readonly ["application/x-adecore-mentio
|
|
|
4
4
|
export declare function carriesMentions(types: readonly string[]): boolean;
|
|
5
5
|
export declare function droppedMentions(data: Pick<DataTransfer, 'getData'>): string[];
|
|
6
6
|
export declare function writeMentionDrag(data: Pick<DataTransfer, 'setData'>, paths: readonly string[], legacy?: boolean): void;
|
|
7
|
+
export declare const CHIPS_CLIPBOARD_TYPE = "application/x-adecore-chips";
|
|
8
|
+
export interface ClipboardChips {
|
|
9
|
+
text: string;
|
|
10
|
+
mentions: string[];
|
|
11
|
+
skills: string[];
|
|
12
|
+
references: ReferenceToken[];
|
|
13
|
+
}
|
|
14
|
+
export declare function clipboardChipsOf(text: string, segments: readonly ChipSegment[]): ClipboardChips;
|
|
15
|
+
export declare function hasClipboardChips(chips: ClipboardChips): boolean;
|
|
16
|
+
export declare function writeClipboardChips(data: Pick<DataTransfer, 'setData'>, chips: ClipboardChips): void;
|
|
17
|
+
export declare function readClipboardChips(data: Pick<DataTransfer, 'getData'>): ClipboardChips | null;
|
|
7
18
|
export interface MentionQuery {
|
|
8
19
|
start: number;
|
|
9
20
|
query: string;
|
|
10
21
|
}
|
|
22
|
+
export interface ReferenceToken {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
11
26
|
export type ChipSegment = {
|
|
12
27
|
kind: 'text';
|
|
13
28
|
text: string;
|
|
@@ -17,22 +32,28 @@ export type ChipSegment = {
|
|
|
17
32
|
} | {
|
|
18
33
|
kind: 'skill';
|
|
19
34
|
name: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'reference';
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
20
39
|
};
|
|
40
|
+
export declare function queryOutsideChips(query: MentionQuery | null, chips: readonly ChipRange[]): MentionQuery | null;
|
|
21
41
|
export declare function findMentionQuery(text: string, caret: number): MentionQuery | null;
|
|
22
42
|
export declare function findSkillQuery(text: string, caret: number): MentionQuery | null;
|
|
43
|
+
export declare function findReferenceQuery(text: string, caret: number): MentionQuery | null;
|
|
23
44
|
export declare function insertToken(text: string, query: MentionQuery, sigil: string, value: string): {
|
|
24
45
|
text: string;
|
|
25
46
|
caret: number;
|
|
26
47
|
};
|
|
27
|
-
export declare function
|
|
48
|
+
export declare function insertMention(text: string, query: MentionQuery, path: string): {
|
|
28
49
|
text: string;
|
|
29
50
|
caret: number;
|
|
30
51
|
};
|
|
31
|
-
export declare function
|
|
52
|
+
export declare function insertSkill(text: string, query: MentionQuery, name: string): {
|
|
32
53
|
text: string;
|
|
33
54
|
caret: number;
|
|
34
55
|
};
|
|
35
|
-
export declare function
|
|
56
|
+
export declare function insertReference(text: string, query: MentionQuery, name: string): {
|
|
36
57
|
text: string;
|
|
37
58
|
caret: number;
|
|
38
59
|
};
|
|
@@ -41,16 +62,10 @@ export interface MentionSearch {
|
|
|
41
62
|
files: string[];
|
|
42
63
|
}
|
|
43
64
|
export declare const NO_MENTION_SEARCH: MentionSearch;
|
|
44
|
-
export
|
|
45
|
-
kind: 'chat';
|
|
46
|
-
chat: T;
|
|
47
|
-
} | {
|
|
48
|
-
kind: 'file';
|
|
49
|
-
path: string;
|
|
50
|
-
};
|
|
51
|
-
export declare function mentionPick<T>(index: number, chats: readonly T[], search: MentionSearch, query: string): MentionPick<T> | null;
|
|
65
|
+
export declare function mentionPick(index: number, search: MentionSearch, query: string): string | null;
|
|
52
66
|
export declare function presentMentions(text: string, chosen: string[]): string[];
|
|
53
67
|
export declare function presentSkills(text: string, chosen: string[]): string[];
|
|
68
|
+
export declare function presentReferences(text: string, ids: readonly string[], tokens: readonly ReferenceToken[]): string[];
|
|
54
69
|
export declare function chipText(segment: ChipSegment): string;
|
|
55
70
|
export declare function pastedMentions(text: string): string[];
|
|
56
71
|
export interface TextRange {
|
|
@@ -58,8 +73,10 @@ export interface TextRange {
|
|
|
58
73
|
to: number;
|
|
59
74
|
}
|
|
60
75
|
export interface ChipRange extends TextRange {
|
|
61
|
-
kind: 'mention' | 'skill';
|
|
76
|
+
kind: 'mention' | 'skill' | 'reference';
|
|
62
77
|
value: string;
|
|
63
78
|
}
|
|
64
|
-
export declare function chipRanges(text: string, mentions: string[], skills?: string[], excluded?: readonly TextRange[]): ChipRange[];
|
|
65
|
-
export declare function tokenizeChips(text: string, mentions: string[], skills?: string[]): ChipSegment[];
|
|
79
|
+
export declare function chipRanges(text: string, mentions: string[], skills?: string[], excluded?: readonly TextRange[], references?: readonly ReferenceToken[]): ChipRange[];
|
|
80
|
+
export declare function tokenizeChips(text: string, mentions: string[], skills?: string[], references?: readonly ReferenceToken[]): ChipSegment[];
|
|
81
|
+
export declare function namedReferenceRanges(text: string, items: readonly ReferenceToken[], referenced?: readonly string[], excluded?: readonly TextRange[]): ChipRange[];
|
|
82
|
+
export declare function tokenizeNamedReferences(text: string, items: readonly ReferenceToken[], referenced?: readonly string[]): ChipSegment[];
|
package/dist/chat/mentions.js
CHANGED
|
@@ -19,17 +19,91 @@ export function writeMentionDrag(data, paths, legacy = true) {
|
|
|
19
19
|
data.setData(LEGACY_MENTION_DRAG_TYPE, text);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
/* Beside the plain text of a copy from a thread or a composer: the chips in it, so a paste makes them chips again. */
|
|
23
|
+
export const CHIPS_CLIPBOARD_TYPE = 'application/x-adecore-chips';
|
|
24
|
+
/* The chips among `segments`, without doubles, for the copy of `text`. */
|
|
25
|
+
export function clipboardChipsOf(text, segments) {
|
|
26
|
+
const chips = {
|
|
27
|
+
text,
|
|
28
|
+
mentions: [],
|
|
29
|
+
skills: [],
|
|
30
|
+
references: []
|
|
31
|
+
};
|
|
32
|
+
for (const segment of segments) {
|
|
33
|
+
if (segment.kind === 'mention' && !chips.mentions.includes(segment.path)) {
|
|
34
|
+
chips.mentions.push(segment.path);
|
|
35
|
+
} else if (segment.kind === 'skill' && !chips.skills.includes(segment.name)) {
|
|
36
|
+
chips.skills.push(segment.name);
|
|
37
|
+
} else if (segment.kind === 'reference' && !chips.references.some(token => token.id === segment.id)) {
|
|
38
|
+
chips.references.push({
|
|
39
|
+
id: segment.id,
|
|
40
|
+
name: segment.name
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return chips;
|
|
45
|
+
}
|
|
46
|
+
export function hasClipboardChips(chips) {
|
|
47
|
+
return chips.mentions.length > 0 || chips.skills.length > 0 || chips.references.length > 0;
|
|
48
|
+
}
|
|
49
|
+
export function writeClipboardChips(data, chips) {
|
|
50
|
+
data.setData('text/plain', chips.text);
|
|
51
|
+
data.setData(CHIPS_CLIPBOARD_TYPE, JSON.stringify(chips));
|
|
52
|
+
}
|
|
53
|
+
const lines = text => text.replace(/\r\n/g, '\n');
|
|
54
|
+
const isStrings = value => Array.isArray(value) && value.every(entry => typeof entry === 'string');
|
|
55
|
+
/* The chips of a paste, or null when it has none or its plain text is no longer the text they were copied with. */
|
|
56
|
+
export function readClipboardChips(data) {
|
|
57
|
+
const raw = data.getData(CHIPS_CLIPBOARD_TYPE);
|
|
58
|
+
if (raw === '') {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(raw);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const {
|
|
71
|
+
text,
|
|
72
|
+
mentions,
|
|
73
|
+
skills,
|
|
74
|
+
references
|
|
75
|
+
} = parsed;
|
|
76
|
+
const tokens = Array.isArray(references) ? references.filter(token => typeof token === 'object' && token !== null && typeof token.id === 'string' && typeof token.name === 'string' && token.name !== '') : [];
|
|
77
|
+
if (typeof text !== 'string' || lines(text) !== lines(data.getData('text/plain')) || !isStrings(mentions) || !isStrings(skills)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
text: lines(text),
|
|
82
|
+
mentions,
|
|
83
|
+
skills,
|
|
84
|
+
references: tokens.map(({
|
|
85
|
+
id,
|
|
86
|
+
name
|
|
87
|
+
}) => ({
|
|
88
|
+
id,
|
|
89
|
+
name
|
|
90
|
+
}))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
22
93
|
function isBoundary(char) {
|
|
23
94
|
return char === undefined || /\s/.test(char);
|
|
24
95
|
}
|
|
96
|
+
const TOKEN_END = /[.,;:!?)\]}"'”’]/;
|
|
25
97
|
// A path at the end of a sentence still counts, so "open @a.ts." is a mention of a.ts.
|
|
26
98
|
function isTokenEnd(char) {
|
|
27
|
-
return isBoundary(char) ||
|
|
99
|
+
return isBoundary(char) || TOKEN_END.test(char);
|
|
28
100
|
}
|
|
101
|
+
// A path goes on after a dot or a colon (`a.ts`, `C:`), so only the rest ends a file query.
|
|
102
|
+
const PATH_GOES_ON = /[.:]$/;
|
|
29
103
|
/*
|
|
30
104
|
* The `<sigil>word` the caret sits in, if any. The sigil has to open a word (start of text or after
|
|
31
|
-
* whitespace) so an email address never opens the picker, and the word may not have been
|
|
32
|
-
*
|
|
105
|
+
* whitespace) so an email address never opens the picker, and the word may not have been closed
|
|
106
|
+
* with whitespace or the punctuation that ends a token (`#Admin?`) yet.
|
|
33
107
|
*/
|
|
34
108
|
function findQuery(text, caret, sigil) {
|
|
35
109
|
const before = text.slice(0, caret);
|
|
@@ -41,11 +115,18 @@ function findQuery(text, caret, sigil) {
|
|
|
41
115
|
if (/\s/.test(query) || !isBoundary(text[caret])) {
|
|
42
116
|
return null;
|
|
43
117
|
}
|
|
118
|
+
if (query !== '' && isTokenEnd(query.at(-1)) && !(sigil === '@' && PATH_GOES_ON.test(query))) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
44
121
|
return {
|
|
45
122
|
start: at,
|
|
46
123
|
query
|
|
47
124
|
};
|
|
48
125
|
}
|
|
126
|
+
/* The query, unless its sigil opens a token that is already a chip, which the picker has nothing to add to. */
|
|
127
|
+
export function queryOutsideChips(query, chips) {
|
|
128
|
+
return query !== null && chips.some(chip => chip.from === query.start) ? null : query;
|
|
129
|
+
}
|
|
49
130
|
export function findMentionQuery(text, caret) {
|
|
50
131
|
return findQuery(text, caret, '@');
|
|
51
132
|
}
|
|
@@ -54,6 +135,14 @@ export function findSkillQuery(text, caret) {
|
|
|
54
135
|
const found = findQuery(text, caret, '$');
|
|
55
136
|
return found === null || found.query !== '' && !/^[A-Za-z]/.test(found.query) ? null : found;
|
|
56
137
|
}
|
|
138
|
+
/*
|
|
139
|
+
* The `#word` the caret sits in. It opens only once a letter follows the sigil, so a `# ` heading,
|
|
140
|
+
* `#123` and a bare `#` stay text. A name with a space is found by its first word.
|
|
141
|
+
*/
|
|
142
|
+
export function findReferenceQuery(text, caret) {
|
|
143
|
+
const found = findQuery(text, caret, '#');
|
|
144
|
+
return found === null || !/^\p{L}/u.test(found.query) ? null : found;
|
|
145
|
+
}
|
|
57
146
|
/* Replaces the `<sigil>query` under the caret with the chosen token and a space, and says where the caret goes. */
|
|
58
147
|
export function insertToken(text, query, sigil, value) {
|
|
59
148
|
const end = query.start + 1 + query.query.length;
|
|
@@ -63,40 +152,25 @@ export function insertToken(text, query, sigil, value) {
|
|
|
63
152
|
caret: query.start + token.length
|
|
64
153
|
};
|
|
65
154
|
}
|
|
66
|
-
/* Takes the `<sigil>query` under the caret out, for a pick that lands beside the text instead of in it. */
|
|
67
|
-
export function dropQuery(text, query) {
|
|
68
|
-
return {
|
|
69
|
-
text: `${text.slice(0, query.start)}${text.slice(query.start + 1 + query.query.length)}`,
|
|
70
|
-
caret: query.start
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
155
|
export function insertMention(text, query, path) {
|
|
74
156
|
return insertToken(text, query, '@', path);
|
|
75
157
|
}
|
|
76
158
|
export function insertSkill(text, query, name) {
|
|
77
159
|
return insertToken(text, query, '$', name);
|
|
78
160
|
}
|
|
161
|
+
export function insertReference(text, query, name) {
|
|
162
|
+
return insertToken(text, query, '#', name);
|
|
163
|
+
}
|
|
79
164
|
export const NO_MENTION_SEARCH = {
|
|
80
165
|
query: null,
|
|
81
166
|
files: []
|
|
82
167
|
};
|
|
83
|
-
/*
|
|
84
|
-
export function mentionPick(index,
|
|
85
|
-
const chat = chats[index];
|
|
86
|
-
if (chat !== undefined) {
|
|
87
|
-
return {
|
|
88
|
-
kind: 'chat',
|
|
89
|
-
chat
|
|
90
|
-
};
|
|
91
|
-
}
|
|
168
|
+
/* The path Enter or Tab takes from the `@` list at `index`. Files still on screen from an older query are never taken. */
|
|
169
|
+
export function mentionPick(index, search, query) {
|
|
92
170
|
if (search.query !== query) {
|
|
93
171
|
return null;
|
|
94
172
|
}
|
|
95
|
-
|
|
96
|
-
return path === undefined ? null : {
|
|
97
|
-
kind: 'file',
|
|
98
|
-
path
|
|
99
|
-
};
|
|
173
|
+
return search.files[index] ?? search.files[0] ?? null;
|
|
100
174
|
}
|
|
101
175
|
function findToken(text, sigil, value, from) {
|
|
102
176
|
const token = `${sigil}${value}`;
|
|
@@ -130,6 +204,11 @@ export function presentMentions(text, chosen) {
|
|
|
130
204
|
export function presentSkills(text, chosen) {
|
|
131
205
|
return present(text, '$', chosen);
|
|
132
206
|
}
|
|
207
|
+
/* The ids among `ids` whose `#name` still sits in the text as a whole token, in text order and without doubles. */
|
|
208
|
+
export function presentReferences(text, ids, tokens) {
|
|
209
|
+
const wanted = new Set(ids);
|
|
210
|
+
return chipRanges(text, [], [], [], tokens).map(range => range.value).filter((id, index, found) => wanted.has(id) && found.indexOf(id) === index);
|
|
211
|
+
}
|
|
133
212
|
/* The characters a segment stands for, sigil included; joined, the segments spell the text they were cut from. */
|
|
134
213
|
export function chipText(segment) {
|
|
135
214
|
if (segment.kind === 'mention') {
|
|
@@ -138,6 +217,9 @@ export function chipText(segment) {
|
|
|
138
217
|
if (segment.kind === 'skill') {
|
|
139
218
|
return `$${segment.name}`;
|
|
140
219
|
}
|
|
220
|
+
if (segment.kind === 'reference') {
|
|
221
|
+
return `#${segment.name}`;
|
|
222
|
+
}
|
|
141
223
|
return segment.text;
|
|
142
224
|
}
|
|
143
225
|
/*
|
|
@@ -152,29 +234,36 @@ export function pastedMentions(text) {
|
|
|
152
234
|
return [...new Set(tokens.map(token => token.slice(1)))];
|
|
153
235
|
}
|
|
154
236
|
/*
|
|
155
|
-
* Where the chosen mentions and
|
|
156
|
-
* prefixes another. A token that overlaps an excluded range (a code span, say) is text.
|
|
237
|
+
* Where the chosen mentions, skills and references sit in the text, in text order; longer values win
|
|
238
|
+
* when one prefixes another. A token that overlaps an excluded range (a code span, say) is text.
|
|
157
239
|
*/
|
|
158
|
-
export function chipRanges(text, mentions, skills = [], excluded = []) {
|
|
240
|
+
export function chipRanges(text, mentions, skills = [], excluded = [], references = []) {
|
|
159
241
|
const tokens = [...[...new Set(mentions)].map(value => ({
|
|
160
242
|
sigil: '@',
|
|
161
243
|
kind: 'mention',
|
|
244
|
+
spelled: value,
|
|
162
245
|
value
|
|
163
246
|
})), ...[...new Set(skills)].map(value => ({
|
|
164
247
|
sigil: '$',
|
|
165
248
|
kind: 'skill',
|
|
249
|
+
spelled: value,
|
|
166
250
|
value
|
|
167
|
-
}))
|
|
251
|
+
})), ...references.filter(token => token.name !== '').map(token => ({
|
|
252
|
+
sigil: '#',
|
|
253
|
+
kind: 'reference',
|
|
254
|
+
spelled: token.name,
|
|
255
|
+
value: token.id
|
|
256
|
+
}))].sort((a, b) => b.spelled.length - a.spelled.length);
|
|
168
257
|
const allowed = (from, to) => !excluded.some(range => from < range.to && to > range.from);
|
|
169
258
|
const ranges = [];
|
|
170
259
|
let cursor = 0;
|
|
171
260
|
while (cursor < text.length) {
|
|
172
261
|
let best = null;
|
|
173
262
|
for (const token of tokens) {
|
|
174
|
-
const length = token.
|
|
175
|
-
let index = findToken(text, token.sigil, token.
|
|
263
|
+
const length = token.spelled.length + 1;
|
|
264
|
+
let index = findToken(text, token.sigil, token.spelled, cursor);
|
|
176
265
|
while (index >= 0 && !allowed(index, index + length)) {
|
|
177
|
-
index = findToken(text, token.sigil, token.
|
|
266
|
+
index = findToken(text, token.sigil, token.spelled, index + 1);
|
|
178
267
|
}
|
|
179
268
|
if (index >= 0 && (best === null || index < best.from)) {
|
|
180
269
|
best = {
|
|
@@ -193,24 +282,80 @@ export function chipRanges(text, mentions, skills = [], excluded = []) {
|
|
|
193
282
|
}
|
|
194
283
|
return ranges;
|
|
195
284
|
}
|
|
196
|
-
|
|
197
|
-
|
|
285
|
+
function segmentOf(text, range) {
|
|
286
|
+
if (range.kind === 'mention') {
|
|
287
|
+
return {
|
|
288
|
+
kind: 'mention',
|
|
289
|
+
path: range.value
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (range.kind === 'skill') {
|
|
293
|
+
return {
|
|
294
|
+
kind: 'skill',
|
|
295
|
+
name: range.value
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
kind: 'reference',
|
|
300
|
+
id: range.value,
|
|
301
|
+
name: text.slice(range.from + 1, range.to)
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/* Splits the text so a renderer can draw the mentions, skills and references as chips. */
|
|
305
|
+
export function tokenizeChips(text, mentions, skills = [], references = []) {
|
|
306
|
+
return segmentsOf(text, chipRanges(text, mentions, skills, [], references));
|
|
307
|
+
}
|
|
308
|
+
// What a `#` may follow in a text nobody typed through the picker: a space, or an opening bracket or quote.
|
|
309
|
+
const REFERENCE_OPENER = /[\s([{"'“‘]/;
|
|
310
|
+
/*
|
|
311
|
+
* Where a text written without the picker, such as a reply, spells `#Name` for a view, node or chat
|
|
312
|
+
* by exactly its name. The longest name wins, so a name with a space is found whole. A name more
|
|
313
|
+
* than one of them carries stays text, unless one of those is in `referenced`, which then wins.
|
|
314
|
+
*/
|
|
315
|
+
export function namedReferenceRanges(text, items, referenced = [], excluded = []) {
|
|
316
|
+
const byName = new Map();
|
|
317
|
+
for (const item of items) {
|
|
318
|
+
if (item.name !== '') {
|
|
319
|
+
byName.set(item.name, [...(byName.get(item.name) ?? []), item.id]);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const names = [...byName.keys()].sort((a, b) => b.length - a.length);
|
|
323
|
+
const preferred = new Set(referenced);
|
|
324
|
+
const ranges = [];
|
|
325
|
+
let index = text.indexOf('#');
|
|
326
|
+
while (index >= 0) {
|
|
327
|
+
const from = index;
|
|
328
|
+
const name = from > 0 && !REFERENCE_OPENER.test(text[from - 1]) ? undefined : names.find(candidate => text.startsWith(candidate, from + 1) && isTokenEnd(text[from + 1 + candidate.length]));
|
|
329
|
+
const to = name === undefined ? from + 1 : from + 1 + name.length;
|
|
330
|
+
const ids = name === undefined ? [] : byName.get(name);
|
|
331
|
+
const id = ids.find(candidate => preferred.has(candidate)) ?? (ids.length === 1 ? ids[0] : undefined);
|
|
332
|
+
if (id !== undefined && !excluded.some(range => from < range.to && to > range.from)) {
|
|
333
|
+
ranges.push({
|
|
334
|
+
from,
|
|
335
|
+
to,
|
|
336
|
+
kind: 'reference',
|
|
337
|
+
value: id
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
index = text.indexOf('#', to);
|
|
341
|
+
}
|
|
342
|
+
return ranges;
|
|
343
|
+
}
|
|
344
|
+
/* `namedReferenceRanges` as segments, for a renderer. */
|
|
345
|
+
export function tokenizeNamedReferences(text, items, referenced = []) {
|
|
346
|
+
return segmentsOf(text, namedReferenceRanges(text, items, referenced));
|
|
347
|
+
}
|
|
348
|
+
function segmentsOf(text, ranges) {
|
|
198
349
|
const segments = [];
|
|
199
350
|
let cursor = 0;
|
|
200
|
-
for (const range of
|
|
351
|
+
for (const range of ranges) {
|
|
201
352
|
if (range.from > cursor) {
|
|
202
353
|
segments.push({
|
|
203
354
|
kind: 'text',
|
|
204
355
|
text: text.slice(cursor, range.from)
|
|
205
356
|
});
|
|
206
357
|
}
|
|
207
|
-
segments.push(range
|
|
208
|
-
kind: 'mention',
|
|
209
|
-
path: range.value
|
|
210
|
-
} : {
|
|
211
|
-
kind: 'skill',
|
|
212
|
-
name: range.value
|
|
213
|
-
});
|
|
358
|
+
segments.push(segmentOf(text, range));
|
|
214
359
|
cursor = range.to;
|
|
215
360
|
}
|
|
216
361
|
if (cursor < text.length) {
|
package/dist/chat/stash.d.ts
CHANGED
package/dist/chat/stash.js
CHANGED
|
@@ -57,7 +57,8 @@ export function parseStash(raw) {
|
|
|
57
57
|
chats: strings(entry.chats),
|
|
58
58
|
attachments: attachments(entry.attachments),
|
|
59
59
|
...stashedQuoteField(entry.quote),
|
|
60
|
-
createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : 0
|
|
60
|
+
createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : 0,
|
|
61
|
+
references: strings(entry.references)
|
|
61
62
|
})).slice(0, STASH_LIMIT);
|
|
62
63
|
} catch {
|
|
63
64
|
return [];
|
|
@@ -94,7 +95,8 @@ export function stashedFrom(draft, id, now, chatId) {
|
|
|
94
95
|
mime: attachment.mime,
|
|
95
96
|
size: uploadBytes(attachment)
|
|
96
97
|
})),
|
|
97
|
-
createdAt: now
|
|
98
|
+
createdAt: now,
|
|
99
|
+
references: [...draft.references]
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
102
|
/* The limit of `chats` in a send. */
|