@nextclaw/ui 0.11.18 → 0.11.20
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 +26 -0
- package/dist/assets/{ChannelsList-eZfHzvxb.js → ChannelsList-DAx7wv0_.js} +1 -1
- package/dist/assets/{ChatPage-DKD5hcD8.js → ChatPage-l2PYwCeB.js} +8 -8
- package/dist/assets/{MarketplacePage-D0iqC5o7.js → MarketplacePage-Dlp5BgCh.js} +1 -1
- package/dist/assets/MarketplacePage-TVeyVOuO.js +1 -0
- package/dist/assets/{McpMarketplacePage-CCmRjGwl.js → McpMarketplacePage-CwKtAil8.js} +1 -1
- package/dist/assets/{ModelConfig-BiWp8Ymp.js → ModelConfig-Dg6F3Ldb.js} +1 -1
- package/dist/assets/{ProvidersList-HaCAzF9F.js → ProvidersList-f7bQdRxA.js} +1 -1
- package/dist/assets/{RemoteAccessPage-DOF4oEHW.js → RemoteAccessPage-w_dY7P4T.js} +1 -1
- package/dist/assets/{RuntimeConfig-BnkWf6Eb.js → RuntimeConfig-M4OKjmgU.js} +1 -1
- package/dist/assets/{SearchConfig-3ofKM9W4.js → SearchConfig-v46R5a2U.js} +1 -1
- package/dist/assets/{SecretsConfig-BRbC2hfo.js → SecretsConfig-CXvUpbB_.js} +1 -1
- package/dist/assets/{SessionsConfig-BpoD_0WD.js → SessionsConfig-7vUHMtOh.js} +1 -1
- package/dist/assets/{index-CjPeKafH.js → index-B0DzQqwv.js} +2 -2
- package/dist/assets/index-BahpXJg8.css +1 -0
- package/dist/assets/{security-config-BcbOF17w.js → security-config-Xi5DYW7j.js} +1 -1
- package/dist/assets/{useConfirmDialog-Dk15Fj1n.js → useConfirmDialog-CXDAxtRL.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +6 -6
- package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +103 -1
- package/src/components/chat/adapters/chat-input-bar.adapter.ts +80 -2
- package/src/components/chat/adapters/chat-message-inline-content.adapter.ts +95 -0
- package/src/components/chat/adapters/chat-message-part.adapter.ts +384 -0
- package/src/components/chat/adapters/chat-message.adapter.test.ts +37 -0
- package/src/components/chat/adapters/chat-message.adapter.ts +18 -366
- package/src/components/chat/chat-composer-state.test.ts +2 -6
- package/src/components/chat/chat-composer-state.ts +27 -6
- package/src/components/chat/chat-inline-token.utils.test.ts +87 -0
- package/src/components/chat/chat-inline-token.utils.ts +146 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +24 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.ts +81 -44
- package/src/components/chat/chat-recent-skills.manager.ts +8 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +31 -4
- package/src/components/chat/containers/chat-message-list.container.test.tsx +45 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +11 -5
- package/src/components/chat/ncp/NcpChatPage.tsx +10 -1
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +18 -4
- package/src/components/chat/presenter/chat-presenter-context.tsx +1 -0
- package/dist/assets/MarketplacePage-CMPjqEmN.js +0 -1
- package/dist/assets/index-DMy_fKKh.css +0 -1
|
@@ -39,6 +39,36 @@ describe('buildChatSlashItems', () => {
|
|
|
39
39
|
const items = buildChatSlashItems([createSkillRecord({ key: 'weather' })], 'terminal', texts);
|
|
40
40
|
expect(items).toEqual([]);
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
it('pushes recent skills ahead when the match strength is the same', () => {
|
|
44
|
+
const items = buildChatSlashItems(
|
|
45
|
+
[
|
|
46
|
+
createSkillRecord({ key: 'docs', label: 'Docs' }),
|
|
47
|
+
createSkillRecord({ key: 'web-search', label: 'Web Search' }),
|
|
48
|
+
createSkillRecord({ key: 'weather', label: 'Weather' })
|
|
49
|
+
],
|
|
50
|
+
'',
|
|
51
|
+
texts,
|
|
52
|
+
['weather', 'docs']
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(items.map((item) => item.value)).toEqual(['weather', 'docs', 'web-search']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('lets recent skills win inside the same slash match tier', () => {
|
|
59
|
+
const items = buildChatSlashItems(
|
|
60
|
+
[
|
|
61
|
+
createSkillRecord({ key: 'web-search', label: 'Web Search' }),
|
|
62
|
+
createSkillRecord({ key: 'weather', label: 'Web Weather' }),
|
|
63
|
+
createSkillRecord({ key: 'docs', label: 'Docs for web' })
|
|
64
|
+
],
|
|
65
|
+
'web',
|
|
66
|
+
texts,
|
|
67
|
+
['weather']
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(items.map((item) => item.value)).toEqual(['weather', 'web-search', 'docs']);
|
|
71
|
+
});
|
|
42
72
|
});
|
|
43
73
|
|
|
44
74
|
describe('buildSelectedSkillItems', () => {
|
|
@@ -60,6 +90,8 @@ describe('buildSkillPickerModel', () => {
|
|
|
60
90
|
const onSelectedKeysChange = vi.fn();
|
|
61
91
|
const model = buildSkillPickerModel({
|
|
62
92
|
skillRecords: [createSkillRecord({ key: 'web-search', label: 'Web Search', description: 'Search web' })],
|
|
93
|
+
recentSkillValues: [],
|
|
94
|
+
groupedRecentSkillValues: [],
|
|
63
95
|
selectedSkills: ['web-search'],
|
|
64
96
|
isLoading: false,
|
|
65
97
|
onSelectedKeysChange,
|
|
@@ -68,7 +100,9 @@ describe('buildSkillPickerModel', () => {
|
|
|
68
100
|
searchPlaceholder: 'Search skills',
|
|
69
101
|
emptyLabel: 'No skills',
|
|
70
102
|
loadingLabel: 'Loading',
|
|
71
|
-
manageLabel: 'Manage'
|
|
103
|
+
manageLabel: 'Manage',
|
|
104
|
+
recentSkillsLabel: 'Recent',
|
|
105
|
+
allSkillsLabel: 'All skills'
|
|
72
106
|
}
|
|
73
107
|
});
|
|
74
108
|
|
|
@@ -82,6 +116,74 @@ describe('buildSkillPickerModel', () => {
|
|
|
82
116
|
label: 'Web Search'
|
|
83
117
|
});
|
|
84
118
|
});
|
|
119
|
+
|
|
120
|
+
it('groups recent skills ahead of the remaining catalog', () => {
|
|
121
|
+
const model = buildSkillPickerModel({
|
|
122
|
+
skillRecords: [
|
|
123
|
+
createSkillRecord({ key: 'docs', label: 'Docs' }),
|
|
124
|
+
createSkillRecord({ key: 'web-search', label: 'Web Search' }),
|
|
125
|
+
createSkillRecord({ key: 'weather', label: 'Weather' })
|
|
126
|
+
],
|
|
127
|
+
recentSkillValues: ['weather', 'docs'],
|
|
128
|
+
groupedRecentSkillValues: ['weather', 'docs'],
|
|
129
|
+
selectedSkills: ['weather'],
|
|
130
|
+
isLoading: false,
|
|
131
|
+
onSelectedKeysChange: vi.fn(),
|
|
132
|
+
texts: {
|
|
133
|
+
title: 'Skills',
|
|
134
|
+
searchPlaceholder: 'Search skills',
|
|
135
|
+
emptyLabel: 'No skills',
|
|
136
|
+
loadingLabel: 'Loading',
|
|
137
|
+
manageLabel: 'Manage',
|
|
138
|
+
recentSkillsLabel: 'Recent',
|
|
139
|
+
allSkillsLabel: 'All skills'
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(model.options.map((option) => option.key)).toEqual(['weather', 'docs', 'web-search']);
|
|
144
|
+
expect(model.groups).toEqual([
|
|
145
|
+
{
|
|
146
|
+
key: 'recent-skills',
|
|
147
|
+
label: 'Recent',
|
|
148
|
+
options: [
|
|
149
|
+
expect.objectContaining({ key: 'weather', label: 'Weather' }),
|
|
150
|
+
expect.objectContaining({ key: 'docs', label: 'Docs' })
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
key: 'all-skills',
|
|
155
|
+
label: 'All skills',
|
|
156
|
+
options: [expect.objectContaining({ key: 'web-search', label: 'Web Search' })]
|
|
157
|
+
}
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('still reorders recent skills even when grouped labels are omitted', () => {
|
|
162
|
+
const model = buildSkillPickerModel({
|
|
163
|
+
skillRecords: [
|
|
164
|
+
createSkillRecord({ key: 'docs', label: 'Docs' }),
|
|
165
|
+
createSkillRecord({ key: 'web-search', label: 'Web Search' }),
|
|
166
|
+
createSkillRecord({ key: 'weather', label: 'Weather' })
|
|
167
|
+
],
|
|
168
|
+
recentSkillValues: ['weather'],
|
|
169
|
+
groupedRecentSkillValues: [],
|
|
170
|
+
selectedSkills: [],
|
|
171
|
+
isLoading: false,
|
|
172
|
+
onSelectedKeysChange: vi.fn(),
|
|
173
|
+
texts: {
|
|
174
|
+
title: 'Skills',
|
|
175
|
+
searchPlaceholder: 'Search skills',
|
|
176
|
+
emptyLabel: 'No skills',
|
|
177
|
+
loadingLabel: 'Loading',
|
|
178
|
+
manageLabel: 'Manage',
|
|
179
|
+
recentSkillsLabel: 'Recent',
|
|
180
|
+
allSkillsLabel: 'All skills'
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(model.options.map((option) => option.key)).toEqual(['weather', 'docs', 'web-search']);
|
|
185
|
+
expect(model.groups).toBeUndefined();
|
|
186
|
+
});
|
|
85
187
|
});
|
|
86
188
|
|
|
87
189
|
describe('buildModelToolbarSelect', () => {
|
|
@@ -44,6 +44,8 @@ export type ChatInputBarAdapterTexts = {
|
|
|
44
44
|
slashSkillSubtitle: string;
|
|
45
45
|
slashSkillSpecLabel: string;
|
|
46
46
|
noSkillDescription: string;
|
|
47
|
+
recentSkillsLabel: string;
|
|
48
|
+
allSkillsLabel: string;
|
|
47
49
|
modelSelectPlaceholder: string;
|
|
48
50
|
modelNoOptionsLabel: string;
|
|
49
51
|
recentModelsLabel: string;
|
|
@@ -129,12 +131,51 @@ function scoreSkillRecord(record: ChatSkillRecord, query: string): number {
|
|
|
129
131
|
return 0;
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
function resolveSlashMatchTier(score: number): number {
|
|
135
|
+
if (score >= SLASH_ITEM_MATCH_SCORE.exactLabel) {
|
|
136
|
+
return 4;
|
|
137
|
+
}
|
|
138
|
+
if (score >= SLASH_ITEM_MATCH_SCORE.prefixToken) {
|
|
139
|
+
return 3;
|
|
140
|
+
}
|
|
141
|
+
if (score >= SLASH_ITEM_MATCH_SCORE.containsLabel) {
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
if (score > 0) {
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildRecentOrderIndex(values: string[]): Map<string, number> {
|
|
151
|
+
return new Map(values.map((value, index) => [value, index] as const));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function prioritizeSkillRecords(skillRecords: ChatSkillRecord[], recentSkillValues: string[]): ChatSkillRecord[] {
|
|
155
|
+
const recentOrderIndex = buildRecentOrderIndex(recentSkillValues);
|
|
156
|
+
const recentRecords: ChatSkillRecord[] = [];
|
|
157
|
+
const remainingRecords: ChatSkillRecord[] = [];
|
|
158
|
+
for (const record of skillRecords) {
|
|
159
|
+
if (recentOrderIndex.has(record.key)) {
|
|
160
|
+
recentRecords.push(record);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
remainingRecords.push(record);
|
|
164
|
+
}
|
|
165
|
+
recentRecords.sort(
|
|
166
|
+
(left, right) => (recentOrderIndex.get(left.key) ?? Number.POSITIVE_INFINITY) - (recentOrderIndex.get(right.key) ?? Number.POSITIVE_INFINITY)
|
|
167
|
+
);
|
|
168
|
+
return [...recentRecords, ...remainingRecords];
|
|
169
|
+
}
|
|
170
|
+
|
|
132
171
|
export function buildChatSlashItems(
|
|
133
172
|
skillRecords: ChatSkillRecord[],
|
|
134
173
|
normalizedSlashQuery: string,
|
|
135
|
-
texts: Pick<ChatInputBarAdapterTexts, 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'noSkillDescription'
|
|
174
|
+
texts: Pick<ChatInputBarAdapterTexts, 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'noSkillDescription'>,
|
|
175
|
+
recentSkillValues: string[] = []
|
|
136
176
|
): ChatSlashItem[] {
|
|
137
177
|
const skillSortCollator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
|
|
178
|
+
const recentOrderIndex = buildRecentOrderIndex(recentSkillValues);
|
|
138
179
|
|
|
139
180
|
return skillRecords
|
|
140
181
|
.map((record, order) => ({
|
|
@@ -144,6 +185,16 @@ export function buildChatSlashItems(
|
|
|
144
185
|
}))
|
|
145
186
|
.filter((entry) => entry.score > 0)
|
|
146
187
|
.sort((left, right) => {
|
|
188
|
+
const leftTier = resolveSlashMatchTier(left.score);
|
|
189
|
+
const rightTier = resolveSlashMatchTier(right.score);
|
|
190
|
+
if (rightTier !== leftTier) {
|
|
191
|
+
return rightTier - leftTier;
|
|
192
|
+
}
|
|
193
|
+
const leftRecentIndex = recentOrderIndex.get(left.record.key) ?? Number.POSITIVE_INFINITY;
|
|
194
|
+
const rightRecentIndex = recentOrderIndex.get(right.record.key) ?? Number.POSITIVE_INFINITY;
|
|
195
|
+
if (leftRecentIndex !== rightRecentIndex) {
|
|
196
|
+
return leftRecentIndex - rightRecentIndex;
|
|
197
|
+
}
|
|
147
198
|
if (right.score !== left.score) {
|
|
148
199
|
return right.score - left.score;
|
|
149
200
|
}
|
|
@@ -189,6 +240,8 @@ export function buildSkillPickerOptions(skillRecords: ChatSkillRecord[]): ChatSk
|
|
|
189
240
|
|
|
190
241
|
export function buildSkillPickerModel(params: {
|
|
191
242
|
skillRecords: ChatSkillRecord[];
|
|
243
|
+
recentSkillValues?: string[];
|
|
244
|
+
groupedRecentSkillValues?: string[];
|
|
192
245
|
selectedSkills: string[];
|
|
193
246
|
isLoading: boolean;
|
|
194
247
|
onSelectedKeysChange: (next: string[]) => void;
|
|
@@ -198,8 +251,18 @@ export function buildSkillPickerModel(params: {
|
|
|
198
251
|
emptyLabel: string;
|
|
199
252
|
loadingLabel: string;
|
|
200
253
|
manageLabel: string;
|
|
254
|
+
recentSkillsLabel: string;
|
|
255
|
+
allSkillsLabel: string;
|
|
201
256
|
};
|
|
202
257
|
}): ChatSkillPickerProps {
|
|
258
|
+
const prioritizedSkillRecords = prioritizeSkillRecords(params.skillRecords, params.recentSkillValues ?? []);
|
|
259
|
+
const recentKeySet = new Set(params.groupedRecentSkillValues ?? []);
|
|
260
|
+
const recentSkillOptions = buildSkillPickerOptions(
|
|
261
|
+
prioritizedSkillRecords.filter((record) => recentKeySet.has(record.key))
|
|
262
|
+
);
|
|
263
|
+
const remainingSkillOptions = buildSkillPickerOptions(
|
|
264
|
+
prioritizedSkillRecords.filter((record) => !recentKeySet.has(record.key))
|
|
265
|
+
);
|
|
203
266
|
return {
|
|
204
267
|
title: params.texts.title,
|
|
205
268
|
searchPlaceholder: params.texts.searchPlaceholder,
|
|
@@ -208,7 +271,22 @@ export function buildSkillPickerModel(params: {
|
|
|
208
271
|
isLoading: params.isLoading,
|
|
209
272
|
manageLabel: params.texts.manageLabel,
|
|
210
273
|
manageHref: '/marketplace/skills',
|
|
211
|
-
options: buildSkillPickerOptions(
|
|
274
|
+
options: buildSkillPickerOptions(prioritizedSkillRecords),
|
|
275
|
+
groups:
|
|
276
|
+
recentSkillOptions.length > 0
|
|
277
|
+
? [
|
|
278
|
+
{
|
|
279
|
+
key: 'recent-skills',
|
|
280
|
+
label: params.texts.recentSkillsLabel,
|
|
281
|
+
options: recentSkillOptions
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
key: 'all-skills',
|
|
285
|
+
label: params.texts.allSkillsLabel,
|
|
286
|
+
options: remainingSkillOptions
|
|
287
|
+
}
|
|
288
|
+
].filter((group) => group.options.length > 0)
|
|
289
|
+
: undefined,
|
|
212
290
|
selectedKeys: params.selectedSkills,
|
|
213
291
|
onSelectedKeysChange: params.onSelectedKeysChange
|
|
214
292
|
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
splitTextByInlineTokens,
|
|
3
|
+
type ChatInlineTokenSource,
|
|
4
|
+
} from "@/components/chat/chat-inline-token.utils";
|
|
5
|
+
import type {
|
|
6
|
+
ChatInlineContentSegmentViewModel,
|
|
7
|
+
ChatMessagePartViewModel,
|
|
8
|
+
} from "@nextclaw/agent-chat-ui";
|
|
9
|
+
import type { ChatMessagePartSource } from "@/components/chat/adapters/chat-message-part.adapter";
|
|
10
|
+
|
|
11
|
+
const INVISIBLE_ONLY_TEXT_PATTERN = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
|
|
12
|
+
|
|
13
|
+
function toRenderableText(value: string): string | null {
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
if (!trimmed) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const visible = trimmed.replace(INVISIBLE_ONLY_TEXT_PATTERN, "").trim();
|
|
19
|
+
return visible ? trimmed : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hasVisibleText(value: string): boolean {
|
|
23
|
+
return value.replace(INVISIBLE_ONLY_TEXT_PATTERN, "").trim().length > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildInlineContentSegments(
|
|
27
|
+
text: string,
|
|
28
|
+
inlineTokens: readonly ChatInlineTokenSource[],
|
|
29
|
+
): ChatInlineContentSegmentViewModel[] | null {
|
|
30
|
+
const fragments = splitTextByInlineTokens(text, inlineTokens);
|
|
31
|
+
if (fragments.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const segments: ChatInlineContentSegmentViewModel[] = [];
|
|
36
|
+
let hasVisibleContent = false;
|
|
37
|
+
|
|
38
|
+
for (const fragment of fragments) {
|
|
39
|
+
if (fragment.type === "token") {
|
|
40
|
+
hasVisibleContent = true;
|
|
41
|
+
segments.push({
|
|
42
|
+
type: "token",
|
|
43
|
+
token: {
|
|
44
|
+
kind: fragment.token.kind,
|
|
45
|
+
key: fragment.token.key,
|
|
46
|
+
label: fragment.token.label,
|
|
47
|
+
rawText: fragment.token.rawText,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (fragment.text.length === 0) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (hasVisibleText(fragment.text)) {
|
|
57
|
+
hasVisibleContent = true;
|
|
58
|
+
}
|
|
59
|
+
segments.push({
|
|
60
|
+
type: "markdown",
|
|
61
|
+
text: fragment.text,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return hasVisibleContent ? segments : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildTextPart(
|
|
69
|
+
part: Extract<ChatMessagePartSource, { type: "text" }>,
|
|
70
|
+
inlineTokens: readonly ChatInlineTokenSource[],
|
|
71
|
+
): Extract<
|
|
72
|
+
ChatMessagePartViewModel,
|
|
73
|
+
{ type: "markdown" | "inline-content" }
|
|
74
|
+
> | null {
|
|
75
|
+
const inlineContent = buildInlineContentSegments(part.text, inlineTokens);
|
|
76
|
+
if (inlineContent && inlineContent.some((segment) => segment.type === "token")) {
|
|
77
|
+
return {
|
|
78
|
+
type: "inline-content",
|
|
79
|
+
segments: inlineContent,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const text = toRenderableText(part.text);
|
|
84
|
+
if (!text) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
type: "markdown",
|
|
89
|
+
text,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildRenderableText(value: string): string | null {
|
|
94
|
+
return toRenderableText(value);
|
|
95
|
+
}
|