@nextclaw/ui 0.11.18 → 0.11.19

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/assets/{ChannelsList-eZfHzvxb.js → ChannelsList-DAx7wv0_.js} +1 -1
  3. package/dist/assets/{ChatPage-DKD5hcD8.js → ChatPage-l2PYwCeB.js} +8 -8
  4. package/dist/assets/{MarketplacePage-D0iqC5o7.js → MarketplacePage-Dlp5BgCh.js} +1 -1
  5. package/dist/assets/MarketplacePage-TVeyVOuO.js +1 -0
  6. package/dist/assets/{McpMarketplacePage-CCmRjGwl.js → McpMarketplacePage-CwKtAil8.js} +1 -1
  7. package/dist/assets/{ModelConfig-BiWp8Ymp.js → ModelConfig-Dg6F3Ldb.js} +1 -1
  8. package/dist/assets/{ProvidersList-HaCAzF9F.js → ProvidersList-f7bQdRxA.js} +1 -1
  9. package/dist/assets/{RemoteAccessPage-DOF4oEHW.js → RemoteAccessPage-w_dY7P4T.js} +1 -1
  10. package/dist/assets/{RuntimeConfig-BnkWf6Eb.js → RuntimeConfig-M4OKjmgU.js} +1 -1
  11. package/dist/assets/{SearchConfig-3ofKM9W4.js → SearchConfig-v46R5a2U.js} +1 -1
  12. package/dist/assets/{SecretsConfig-BRbC2hfo.js → SecretsConfig-CXvUpbB_.js} +1 -1
  13. package/dist/assets/{SessionsConfig-BpoD_0WD.js → SessionsConfig-7vUHMtOh.js} +1 -1
  14. package/dist/assets/{index-CjPeKafH.js → index-B0DzQqwv.js} +2 -2
  15. package/dist/assets/index-BahpXJg8.css +1 -0
  16. package/dist/assets/{security-config-BcbOF17w.js → security-config-Xi5DYW7j.js} +1 -1
  17. package/dist/assets/{useConfirmDialog-Dk15Fj1n.js → useConfirmDialog-CXDAxtRL.js} +1 -1
  18. package/dist/index.html +2 -2
  19. package/package.json +5 -5
  20. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +103 -1
  21. package/src/components/chat/adapters/chat-input-bar.adapter.ts +80 -2
  22. package/src/components/chat/adapters/chat-message-inline-content.adapter.ts +95 -0
  23. package/src/components/chat/adapters/chat-message-part.adapter.ts +384 -0
  24. package/src/components/chat/adapters/chat-message.adapter.test.ts +37 -0
  25. package/src/components/chat/adapters/chat-message.adapter.ts +18 -366
  26. package/src/components/chat/chat-composer-state.test.ts +2 -6
  27. package/src/components/chat/chat-composer-state.ts +27 -6
  28. package/src/components/chat/chat-inline-token.utils.test.ts +87 -0
  29. package/src/components/chat/chat-inline-token.utils.ts +146 -0
  30. package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +24 -0
  31. package/src/components/chat/chat-input/chat-input-bar.controller.ts +81 -44
  32. package/src/components/chat/chat-recent-skills.manager.ts +8 -0
  33. package/src/components/chat/containers/chat-input-bar.container.tsx +31 -4
  34. package/src/components/chat/containers/chat-message-list.container.test.tsx +45 -0
  35. package/src/components/chat/containers/chat-message-list.container.tsx +11 -5
  36. package/src/components/chat/ncp/NcpChatPage.tsx +10 -1
  37. package/src/components/chat/ncp/ncp-chat-input.manager.ts +18 -4
  38. package/src/components/chat/presenter/chat-presenter-context.tsx +1 -0
  39. package/dist/assets/MarketplacePage-CMPjqEmN.js +0 -1
  40. 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(params.skillRecords),
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
+ }