@nextclaw/ui 0.15.14 → 0.15.15
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 +19 -0
- package/dist/assets/{api-DGG69ZcJ.js → api-D5UCLVE_.js} +3 -3
- package/dist/assets/app-presenter-provider-DcpcRgLn.js +106 -0
- package/dist/assets/{appearance-settings-page-Byr55LGb.js → appearance-settings-page-yJR6Kt9c.js} +1 -1
- package/dist/assets/{book-open-BhaS5BJv.js → book-open-WsEnuW9P.js} +1 -1
- package/dist/assets/{card-Bcr5YM8F.js → card-xVrS1b1T.js} +1 -1
- package/dist/assets/{channels-list-page-C_x83ZCT.js → channels-list-page-CAWuTHSM.js} +1 -1
- package/dist/assets/{chat-page-CEavSS6B.js → chat-page-CXYXMIQQ.js} +1 -1
- package/dist/assets/{config-split-page-RXAoLeNv.js → config-split-page-xj0UtMow.js} +1 -1
- package/dist/assets/{confirm-dialog-o_VPKvVD.js → confirm-dialog-e2YCUzt1.js} +1 -1
- package/dist/assets/{desktop-update-config-DEOMOsDN.js → desktop-update-config-D5NIaimo.js} +1 -1
- package/dist/assets/{dist-C87P4QWA.js → dist-Ca-jiFrK.js} +1 -1
- package/dist/assets/doc-browser-B2tNEKrJ.js +1 -0
- package/dist/assets/{doc-browser-DQ75iUkA.js → doc-browser-CZX6I6CU.js} +1 -1
- package/dist/assets/{doc-browser-context-tE0--wSa.js → doc-browser-context-CGz0LULR.js} +1 -1
- package/dist/assets/{eye-DL9QkrKg.js → eye-B30J471A.js} +1 -1
- package/dist/assets/{form-actions-J0tIaUdK.js → form-actions-CnSzu3WD.js} +1 -1
- package/dist/assets/{index-BFIZUZP3.js → index-6Esxu-Pp.js} +2 -2
- package/dist/assets/index-BzSgA8tR.css +1 -0
- package/dist/assets/loader-circle-DjwSRQHW.js +1 -0
- package/dist/assets/{logo-badge-BYTbZbOF.js → logo-badge-CISsltAo.js} +1 -1
- package/dist/assets/mcp-marketplace-page-BZa7p7Qf.js +1 -0
- package/dist/assets/{mcp-marketplace-page-COWlzeP1.js → mcp-marketplace-page-CPcPzMUQ.js} +1 -1
- package/dist/assets/{model-config-page-oNqnMXNY.js → model-config-page-D6ex3_NM.js} +1 -1
- package/dist/assets/{navigation-link-Dk6VdDkc.js → navigation-link-qPPUiLt0.js} +1 -1
- package/dist/assets/plus-C45awBb1.js +1 -0
- package/dist/assets/{popover-Txwrum65.js → popover-DHx55dbc.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-B7rQM4b6.js → provider-scoped-model-input-1TJxAhx6.js} +1 -1
- package/dist/assets/{providers-config-page-CP-OnvLK.js → providers-config-page-BwHtHIPe.js} +1 -1
- package/dist/assets/{react-B0LbRnAE.js → react-DerwD2EA.js} +1 -1
- package/dist/assets/{refresh-cw-C84mMku5.js → refresh-cw-C7bynrWX.js} +1 -1
- package/dist/assets/remote-CPpTKDgX.js +1 -0
- package/dist/assets/rotate-cw-Bhx0XkQP.js +1 -0
- package/dist/assets/{runtime-config-page-CIGtssWi.js → runtime-config-page-CMLVC5UV.js} +1 -1
- package/dist/assets/{save-R_qyrHpa.js → save-CsRsFtHS.js} +1 -1
- package/dist/assets/{search-UKxiouNi.js → search-D0HtJIL-.js} +1 -1
- package/dist/assets/{search-config-page-DzkpSGFH.js → search-config-page-n1GElgij.js} +1 -1
- package/dist/assets/{secrets-config-page-DhoLYm7Z.js → secrets-config-page-DbUc3Ggi.js} +1 -1
- package/dist/assets/security-config-n2WHhHXV.js +1 -0
- package/dist/assets/{select-CYHT8P5S.js → select-C1zfp9zM.js} +1 -1
- package/dist/assets/{settings-2-DGh4bGSi.js → settings-2-Ba3cLSN2.js} +1 -1
- package/dist/assets/skeleton-DL5npsVL.js +1 -0
- package/dist/assets/{tabs-BCihm1zK.js → tabs-DmKgMKzb.js} +1 -1
- package/dist/assets/{tag-chip-De0iz4o2.js → tag-chip-2vwlVUwM.js} +1 -1
- package/dist/assets/{trash-2-Bo3DWiju.js → trash-2-CUpWjNHu.js} +1 -1
- package/dist/assets/{use-config-DCzwgChy.js → use-config-BvxDWV-M.js} +1 -1
- package/dist/assets/{use-confirm-dialog-WZ94CehU.js → use-confirm-dialog-CeUEE4p5.js} +1 -1
- package/dist/assets/{use-viewport-layout-CZ-3pMne.js → use-viewport-layout-DaKJ-kap.js} +1 -1
- package/dist/assets/x-DGdYT7rH.js +1 -0
- package/dist/index.html +28 -28
- package/package.json +9 -9
- package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -1
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +2 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +11 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +4 -0
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +3 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +64 -72
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +85 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +108 -257
- package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +52 -0
- package/src/features/chat/features/conversation/utils/__tests__/session-queued-input.utils.test.ts +63 -0
- package/src/features/chat/features/conversation/utils/session-queued-input.utils.ts +134 -0
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +5 -5
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +113 -32
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +3 -2
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +118 -12
- package/src/features/chat/features/message/components/__tests__/chat-message-list-skill-preview.container.test.tsx +170 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +18 -9
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +36 -23
- package/src/features/chat/features/message/hooks/__tests__/use-chat-message-virtualizer.test.tsx +90 -0
- package/src/features/chat/features/message/hooks/use-chat-message-virtualizer.ts +3 -5
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +51 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +9 -3
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +42 -8
- package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
- package/dist/assets/app-presenter-provider-Dp90n1Va.js +0 -106
- package/dist/assets/doc-browser-BuGh646I.js +0 -1
- package/dist/assets/index-B2YF-7Yw.css +0 -1
- package/dist/assets/loader-circle-CAnHHSrY.js +0 -1
- package/dist/assets/mcp-marketplace-page-CaUFv37E.js +0 -1
- package/dist/assets/plus-DjdTWPjQ.js +0 -1
- package/dist/assets/remote-_L4pTEaW.js +0 -1
- package/dist/assets/rotate-cw-DQJhDnjg.js +0 -1
- package/dist/assets/security-config-yraYoy9s.js +0 -1
- package/dist/assets/skeleton-BQuylYFO.js +0 -1
- package/dist/assets/x-NZxXyqW1.js +0 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createChatComposerTextNode,
|
|
3
|
+
createChatComposerTokenNode,
|
|
4
|
+
normalizeChatComposerNodes,
|
|
5
|
+
type ChatComposerNode,
|
|
6
|
+
} from '@nextclaw/agent-chat-ui';
|
|
7
|
+
import type { UiNcpSessionQueuedInputView } from '@nextclaw/client-sdk';
|
|
8
|
+
import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
readInlineTokensFromMetadata,
|
|
12
|
+
resolveInlineTokensForText,
|
|
13
|
+
type ChatInlineTokenSource,
|
|
14
|
+
} from '@/features/chat/features/input/utils/chat-inline-token.utils';
|
|
15
|
+
import { deriveChatComposerDraft } from '@/features/chat/features/input/utils/chat-composer-state.utils';
|
|
16
|
+
import { normalizeRequestedSkills } from '@/features/chat/features/runtime/utils/chat-runtime.utils';
|
|
17
|
+
import type { SessionConversationComposerState } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
|
|
18
|
+
|
|
19
|
+
export type SessionQueuedInputComposerSnapshot = SessionConversationComposerState & {
|
|
20
|
+
readonly attachments: readonly NcpDraftAttachment[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function buildTextNodes(
|
|
24
|
+
text: string,
|
|
25
|
+
inlineTokens: readonly ChatInlineTokenSource[],
|
|
26
|
+
): ChatComposerNode[] {
|
|
27
|
+
const nodes: ChatComposerNode[] = [];
|
|
28
|
+
let offset = 0;
|
|
29
|
+
while (offset < text.length) {
|
|
30
|
+
const nextToken = inlineTokens
|
|
31
|
+
.flatMap((token) => {
|
|
32
|
+
const index = text.indexOf(token.rawText, offset);
|
|
33
|
+
return index < 0 ? [] : [{ index, token }];
|
|
34
|
+
})
|
|
35
|
+
.sort((left, right) => left.index - right.index || right.token.rawText.length - left.token.rawText.length)[0];
|
|
36
|
+
if (!nextToken) {
|
|
37
|
+
nodes.push(createChatComposerTextNode(text.slice(offset)));
|
|
38
|
+
return nodes;
|
|
39
|
+
}
|
|
40
|
+
if (nextToken.index > offset) {
|
|
41
|
+
nodes.push(createChatComposerTextNode(text.slice(offset, nextToken.index)));
|
|
42
|
+
}
|
|
43
|
+
nodes.push(createChatComposerTokenNode({
|
|
44
|
+
tokenKind: nextToken.token.kind,
|
|
45
|
+
tokenKey: "ref" in nextToken.token ? nextToken.token.ref : nextToken.token.key,
|
|
46
|
+
label: nextToken.token.label,
|
|
47
|
+
}));
|
|
48
|
+
offset = nextToken.index + nextToken.token.rawText.length;
|
|
49
|
+
}
|
|
50
|
+
return nodes;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readInputMetadata(input: UiNcpSessionQueuedInputView): Record<string, unknown> {
|
|
54
|
+
return {
|
|
55
|
+
...input.metadata,
|
|
56
|
+
...input.message.metadata,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readRequestedSkills(input: UiNcpSessionQueuedInputView): string[] {
|
|
61
|
+
const raw = readInputMetadata(input).requested_skill_refs;
|
|
62
|
+
return normalizeRequestedSkills(
|
|
63
|
+
Array.isArray(raw) ? raw.filter((value): value is string => typeof value === 'string') : undefined,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildSessionQueuedInputPreview(input: UiNcpSessionQueuedInputView): string {
|
|
68
|
+
const inlineTokens = readInlineTokensFromMetadata(readInputMetadata(input));
|
|
69
|
+
const text = input.message.parts.flatMap((part) =>
|
|
70
|
+
part.type === 'text' || part.type === 'rich-text' || part.type === 'reasoning'
|
|
71
|
+
? [part.text]
|
|
72
|
+
: [],
|
|
73
|
+
).join(' ');
|
|
74
|
+
const plainText = inlineTokens.reduce(
|
|
75
|
+
(value, token) => value.split(token.rawText).join(''),
|
|
76
|
+
text,
|
|
77
|
+
).replace(/\s+/g, ' ').trim();
|
|
78
|
+
if (plainText) {
|
|
79
|
+
return plainText;
|
|
80
|
+
}
|
|
81
|
+
return input.message.parts.flatMap((part) => part.type === 'file' && part.name ? [part.name] : []).join(', ');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildSessionQueuedInputComposerSnapshot(
|
|
85
|
+
input: UiNcpSessionQueuedInputView,
|
|
86
|
+
availableSkills: readonly { ref: string; name: string }[],
|
|
87
|
+
): SessionQueuedInputComposerSnapshot {
|
|
88
|
+
const nodes: ChatComposerNode[] = [];
|
|
89
|
+
const attachments: NcpDraftAttachment[] = [];
|
|
90
|
+
const metadataTokens = readInlineTokensFromMetadata(readInputMetadata(input));
|
|
91
|
+
|
|
92
|
+
input.message.parts.forEach((part, index) => {
|
|
93
|
+
if (part.type === 'text' || part.type === 'rich-text' || part.type === 'reasoning') {
|
|
94
|
+
nodes.push(...buildTextNodes(
|
|
95
|
+
part.text,
|
|
96
|
+
resolveInlineTokensForText(part.text, metadataTokens),
|
|
97
|
+
));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (part.type !== 'file') {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const id = `queued-attachment-${input.id}-${index}`;
|
|
104
|
+
const attachment: NcpDraftAttachment = {
|
|
105
|
+
id,
|
|
106
|
+
name: part.name ?? 'attachment',
|
|
107
|
+
mimeType: part.mimeType ?? 'application/octet-stream',
|
|
108
|
+
sizeBytes: part.sizeBytes ?? 0,
|
|
109
|
+
assetUri: part.assetUri,
|
|
110
|
+
url: part.url,
|
|
111
|
+
contentBase64: part.contentBase64,
|
|
112
|
+
};
|
|
113
|
+
attachments.push(attachment);
|
|
114
|
+
nodes.push(createChatComposerTokenNode({
|
|
115
|
+
tokenKind: 'file',
|
|
116
|
+
tokenKey: id,
|
|
117
|
+
label: attachment.name,
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const normalizedNodes = normalizeChatComposerNodes(nodes);
|
|
122
|
+
const selectedSkills = readRequestedSkills(input);
|
|
123
|
+
const skillNameByRef = new Map(availableSkills.map(({ ref, name }) => [ref, name]));
|
|
124
|
+
return {
|
|
125
|
+
text: deriveChatComposerDraft(normalizedNodes),
|
|
126
|
+
nodes: normalizedNodes,
|
|
127
|
+
selectedSkills,
|
|
128
|
+
skillRecords: selectedSkills.map((ref) => ({
|
|
129
|
+
ref,
|
|
130
|
+
name: skillNameByRef.get(ref) ?? ref,
|
|
131
|
+
})),
|
|
132
|
+
attachments,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -12,10 +12,10 @@ describe('deriveNcpMessagePartsFromComposer', () => {
|
|
|
12
12
|
label: 'one.png'
|
|
13
13
|
}),
|
|
14
14
|
createChatComposerTextNode(' between '),
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
createChatComposerTokenNode({
|
|
16
|
+
tokenKind: 'skill',
|
|
17
|
+
tokenKey: 'workspace:/tmp/workspace/skills/web-search',
|
|
18
|
+
label: 'Web Search'
|
|
19
19
|
}),
|
|
20
20
|
createChatComposerTextNode('after'),
|
|
21
21
|
createChatComposerTokenNode({
|
|
@@ -56,7 +56,7 @@ describe('deriveNcpMessagePartsFromComposer', () => {
|
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
type: 'text',
|
|
59
|
-
text: ' between $
|
|
59
|
+
text: ' between $Web Searchafter'
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
type: 'file',
|
|
@@ -11,72 +11,151 @@ import {
|
|
|
11
11
|
describe('chat-inline-token utils', () => {
|
|
12
12
|
it('builds ordered inline skill tokens from composer nodes', () => {
|
|
13
13
|
expect(
|
|
14
|
-
buildInlineTokensFromComposer(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
buildInlineTokensFromComposer(
|
|
15
|
+
[
|
|
16
|
+
createChatComposerTextNode('before '),
|
|
17
|
+
createChatComposerTokenNode({
|
|
18
|
+
tokenKind: 'skill',
|
|
19
|
+
tokenKey: 'workspace:/skills/weather',
|
|
20
|
+
label: 'weather'
|
|
21
|
+
}),
|
|
22
|
+
createChatComposerTokenNode({
|
|
23
|
+
tokenKind: 'skill',
|
|
24
|
+
tokenKey: 'global:/skills/docs',
|
|
25
|
+
label: 'docs'
|
|
26
|
+
})
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
{
|
|
30
|
+
ref: 'workspace:/skills/weather',
|
|
31
|
+
name: 'weather',
|
|
32
|
+
source: 'workspace',
|
|
33
|
+
path: '/skills/weather/SKILL.md',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
ref: 'global:/skills/docs',
|
|
37
|
+
name: 'docs',
|
|
38
|
+
source: 'global',
|
|
39
|
+
path: '/skills/docs/SKILL.md',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
)
|
|
27
43
|
).toEqual([
|
|
28
44
|
{
|
|
29
45
|
kind: 'skill',
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
ref: 'workspace:/skills/weather',
|
|
47
|
+
name: 'weather',
|
|
48
|
+
source: 'workspace',
|
|
49
|
+
path: '/skills/weather/SKILL.md',
|
|
50
|
+
label: 'weather',
|
|
32
51
|
rawText: '$weather'
|
|
33
52
|
},
|
|
34
53
|
{
|
|
35
54
|
kind: 'skill',
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
ref: 'global:/skills/docs',
|
|
56
|
+
name: 'docs',
|
|
57
|
+
source: 'global',
|
|
58
|
+
path: '/skills/docs/SKILL.md',
|
|
59
|
+
label: 'docs',
|
|
38
60
|
rawText: '$docs'
|
|
39
61
|
}
|
|
40
62
|
]);
|
|
41
63
|
});
|
|
42
64
|
|
|
43
|
-
it('reads
|
|
65
|
+
it('reads versioned inline skill metadata without parsing its ref', () => {
|
|
44
66
|
expect(
|
|
45
67
|
readInlineTokensFromMetadata({
|
|
46
|
-
[CHAT_INLINE_TOKENS_METADATA_KEY]:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
[CHAT_INLINE_TOKENS_METADATA_KEY]: {
|
|
69
|
+
schemaVersion: 2,
|
|
70
|
+
items: [
|
|
71
|
+
{
|
|
72
|
+
kind: 'skill',
|
|
73
|
+
ref: 'workspace:/skills/weather',
|
|
74
|
+
name: 'weather',
|
|
75
|
+
source: 'workspace',
|
|
76
|
+
path: '/skills/weather/SKILL.md',
|
|
77
|
+
label: 'weather',
|
|
78
|
+
rawText: '$weather'
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
54
82
|
})
|
|
55
83
|
).toEqual([
|
|
56
84
|
{
|
|
57
85
|
kind: 'skill',
|
|
58
|
-
|
|
59
|
-
|
|
86
|
+
ref: 'workspace:/skills/weather',
|
|
87
|
+
name: 'weather',
|
|
88
|
+
source: 'workspace',
|
|
89
|
+
path: '/skills/weather/SKILL.md',
|
|
90
|
+
label: 'weather',
|
|
60
91
|
rawText: '$weather'
|
|
61
92
|
}
|
|
62
93
|
]);
|
|
63
94
|
});
|
|
64
95
|
|
|
96
|
+
it('normalizes persisted v1 skill keys only at the metadata boundary', () => {
|
|
97
|
+
expect(readInlineTokensFromMetadata({
|
|
98
|
+
[CHAT_INLINE_TOKENS_METADATA_KEY]: [
|
|
99
|
+
{
|
|
100
|
+
kind: 'skill',
|
|
101
|
+
key: 'workspace:/skills/weather',
|
|
102
|
+
label: 'weather',
|
|
103
|
+
rawText: '$workspace:/skills/weather',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
})).toEqual([
|
|
107
|
+
{
|
|
108
|
+
kind: 'skill',
|
|
109
|
+
ref: 'workspace:/skills/weather',
|
|
110
|
+
name: 'weather',
|
|
111
|
+
source: null,
|
|
112
|
+
path: null,
|
|
113
|
+
label: 'weather',
|
|
114
|
+
rawText: '$workspace:/skills/weather',
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('preserves every skill source as an explicit field', () => {
|
|
120
|
+
const sources = ['builtin', 'global', 'project', 'workspace'] as const;
|
|
121
|
+
const tokens = buildInlineTokensFromComposer(
|
|
122
|
+
sources.map((source) => createChatComposerTokenNode({
|
|
123
|
+
tokenKind: 'skill',
|
|
124
|
+
tokenKey: `${source}:/skills/${source}`,
|
|
125
|
+
label: source,
|
|
126
|
+
})),
|
|
127
|
+
sources.map((source) => ({
|
|
128
|
+
ref: `${source}:/skills/${source}`,
|
|
129
|
+
name: source,
|
|
130
|
+
source,
|
|
131
|
+
path: `/skills/${source}/SKILL.md`,
|
|
132
|
+
})),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(tokens.map((token) => token.kind === 'skill' ? token.source : null)).toEqual(sources);
|
|
136
|
+
});
|
|
137
|
+
|
|
65
138
|
it('merges metadata tokens with pure text protocol tokens', () => {
|
|
66
139
|
expect(
|
|
67
140
|
resolveInlineTokensForText('please use $weather and @panel-app:task-board', [
|
|
68
141
|
{
|
|
69
142
|
kind: 'skill',
|
|
70
|
-
|
|
71
|
-
|
|
143
|
+
ref: 'workspace:/skills/weather',
|
|
144
|
+
name: 'weather',
|
|
145
|
+
source: 'workspace',
|
|
146
|
+
path: '/skills/weather/SKILL.md',
|
|
147
|
+
label: 'weather',
|
|
72
148
|
rawText: '$weather'
|
|
73
149
|
}
|
|
74
150
|
])
|
|
75
151
|
).toEqual([
|
|
76
152
|
{
|
|
77
153
|
kind: 'skill',
|
|
78
|
-
|
|
79
|
-
|
|
154
|
+
ref: 'workspace:/skills/weather',
|
|
155
|
+
name: 'weather',
|
|
156
|
+
source: 'workspace',
|
|
157
|
+
path: '/skills/weather/SKILL.md',
|
|
158
|
+
label: 'weather',
|
|
80
159
|
rawText: '$weather'
|
|
81
160
|
},
|
|
82
161
|
{
|
|
@@ -87,7 +166,9 @@ describe('chat-inline-token utils', () => {
|
|
|
87
166
|
}
|
|
88
167
|
]);
|
|
89
168
|
});
|
|
169
|
+
});
|
|
90
170
|
|
|
171
|
+
describe('chat inline token workspace references', () => {
|
|
91
172
|
it('builds inline panel app tokens from pure text protocol', () => {
|
|
92
173
|
expect(buildInlineTokensFromTextProtocol('review @panel-app:task-board now')).toEqual([
|
|
93
174
|
{
|
|
@@ -9,12 +9,13 @@ const CHAT_WORKSPACE_FILE_TOKEN_PREFIX = '@file:';
|
|
|
9
9
|
const CHAT_WORKSPACE_DIRECTORY_TOKEN_PREFIX = '@folder:';
|
|
10
10
|
|
|
11
11
|
export function serializeChatComposerTokenText(params: {
|
|
12
|
+
label?: string;
|
|
12
13
|
tokenKey: string;
|
|
13
14
|
tokenKind: string;
|
|
14
15
|
}): string | null {
|
|
15
|
-
const { tokenKey, tokenKind } = params;
|
|
16
|
+
const { label, tokenKey, tokenKind } = params;
|
|
16
17
|
if (tokenKind === 'skill') {
|
|
17
|
-
return `${CHAT_SKILL_TOKEN_PREFIX}${tokenKey}`;
|
|
18
|
+
return `${CHAT_SKILL_TOKEN_PREFIX}${label?.trim() || tokenKey}`;
|
|
18
19
|
}
|
|
19
20
|
if (tokenKind === 'panel_app') {
|
|
20
21
|
return `${CHAT_PANEL_APP_TOKEN_PREFIX}${tokenKey}`;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
2
2
|
import {
|
|
3
3
|
CHAT_INLINE_TOKENS_METADATA_KEY,
|
|
4
|
+
CHAT_INLINE_TOKENS_SCHEMA_VERSION,
|
|
4
5
|
CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
|
|
5
6
|
CHAT_WORKSPACE_FILE_TOKEN_KIND,
|
|
6
7
|
type ChatInlineTokenMetadata,
|
|
8
|
+
type ChatInlineTokensMetadata,
|
|
9
|
+
type ChatSkillSource,
|
|
7
10
|
} from '@nextclaw/shared';
|
|
8
11
|
import { serializeChatComposerTokenText } from './chat-composer-token-protocol.utils';
|
|
9
12
|
|
|
@@ -13,7 +16,29 @@ const CHAT_PANEL_APP_TOKEN_PATTERN = /@panel-app:([A-Za-z0-9_-]+)/g;
|
|
|
13
16
|
const CHAT_WORKSPACE_FILE_TOKEN_PATTERN = /@file:([^\s]+)/g;
|
|
14
17
|
const CHAT_WORKSPACE_DIRECTORY_TOKEN_PATTERN = /@folder:([^\s]+)/g;
|
|
15
18
|
|
|
16
|
-
export type
|
|
19
|
+
export type ChatSkillReferenceSnapshot = {
|
|
20
|
+
ref: string;
|
|
21
|
+
name: string;
|
|
22
|
+
source: ChatSkillSource;
|
|
23
|
+
path: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ChatInlineTokenSource =
|
|
27
|
+
| {
|
|
28
|
+
kind: 'skill';
|
|
29
|
+
ref: string;
|
|
30
|
+
name: string;
|
|
31
|
+
source: ChatSkillSource | null;
|
|
32
|
+
path: string | null;
|
|
33
|
+
label: string;
|
|
34
|
+
rawText: string;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
kind: string;
|
|
38
|
+
key: string;
|
|
39
|
+
label: string;
|
|
40
|
+
rawText: string;
|
|
41
|
+
};
|
|
17
42
|
|
|
18
43
|
export function resolveWorkspaceReferencePath(params: {
|
|
19
44
|
projectRoot: string | null | undefined;
|
|
@@ -44,11 +69,12 @@ function readOptionalString(value: unknown): string | null {
|
|
|
44
69
|
return trimmed.length > 0 ? trimmed : null;
|
|
45
70
|
}
|
|
46
71
|
|
|
47
|
-
function dedupeInlineTokens(tokens: readonly
|
|
72
|
+
function dedupeInlineTokens<T extends ChatInlineTokenSource>(tokens: readonly T[]): T[] {
|
|
48
73
|
const seen = new Set<string>();
|
|
49
|
-
const output:
|
|
74
|
+
const output: T[] = [];
|
|
50
75
|
for (const token of tokens) {
|
|
51
|
-
const
|
|
76
|
+
const identity = 'ref' in token ? token.ref : token.key;
|
|
77
|
+
const dedupeKey = `${token.kind}:${identity}:${token.rawText}`;
|
|
52
78
|
if (seen.has(dedupeKey)) {
|
|
53
79
|
continue;
|
|
54
80
|
}
|
|
@@ -58,8 +84,12 @@ function dedupeInlineTokens(tokens: readonly ChatInlineTokenSource[]): ChatInlin
|
|
|
58
84
|
return output;
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
export function buildInlineTokensFromComposer(
|
|
62
|
-
|
|
87
|
+
export function buildInlineTokensFromComposer(
|
|
88
|
+
nodes: readonly ChatComposerNode[],
|
|
89
|
+
skillRecords: readonly ChatSkillReferenceSnapshot[] = [],
|
|
90
|
+
): ChatInlineTokenMetadata[] {
|
|
91
|
+
const skillByRef = new Map(skillRecords.map((record) => [record.ref, record]));
|
|
92
|
+
const tokens: ChatInlineTokenMetadata[] = [];
|
|
63
93
|
for (const node of nodes) {
|
|
64
94
|
if (node.type !== 'token') {
|
|
65
95
|
continue;
|
|
@@ -75,8 +105,27 @@ export function buildInlineTokensFromComposer(nodes: readonly ChatComposerNode[]
|
|
|
75
105
|
if (!rawText) {
|
|
76
106
|
continue;
|
|
77
107
|
}
|
|
108
|
+
if (node.tokenKind === 'skill') {
|
|
109
|
+
const skill = skillByRef.get(node.tokenKey);
|
|
110
|
+
if (!skill?.path.trim()) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
tokens.push({
|
|
114
|
+
kind: 'skill',
|
|
115
|
+
ref: skill.ref,
|
|
116
|
+
name: skill.name,
|
|
117
|
+
source: skill.source,
|
|
118
|
+
path: skill.path,
|
|
119
|
+
label: node.label,
|
|
120
|
+
rawText,
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const workspaceKind = node.tokenKind === CHAT_WORKSPACE_FILE_TOKEN_KIND
|
|
125
|
+
? CHAT_WORKSPACE_FILE_TOKEN_KIND
|
|
126
|
+
: CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND;
|
|
78
127
|
tokens.push({
|
|
79
|
-
kind:
|
|
128
|
+
kind: workspaceKind,
|
|
80
129
|
key: node.tokenKey,
|
|
81
130
|
label: node.label,
|
|
82
131
|
rawText,
|
|
@@ -152,28 +201,85 @@ export function readInlineTokensFromMetadata(
|
|
|
152
201
|
metadata: Record<string, unknown> | undefined
|
|
153
202
|
): ChatInlineTokenSource[] {
|
|
154
203
|
const raw = metadata?.[CHAT_INLINE_TOKENS_METADATA_KEY];
|
|
155
|
-
if (
|
|
204
|
+
if (Array.isArray(raw)) {
|
|
205
|
+
return readLegacyInlineTokens(raw);
|
|
206
|
+
}
|
|
207
|
+
if (
|
|
208
|
+
!isRecord(raw) ||
|
|
209
|
+
raw.schemaVersion !== CHAT_INLINE_TOKENS_SCHEMA_VERSION ||
|
|
210
|
+
!Array.isArray(raw.items)
|
|
211
|
+
) {
|
|
156
212
|
return [];
|
|
157
213
|
}
|
|
158
214
|
|
|
159
215
|
const tokens: ChatInlineTokenSource[] = [];
|
|
160
|
-
for (const entry of raw) {
|
|
216
|
+
for (const entry of raw.items) {
|
|
161
217
|
if (!isRecord(entry)) {
|
|
162
218
|
continue;
|
|
163
219
|
}
|
|
164
220
|
const kind = readOptionalString(entry.kind);
|
|
165
|
-
const key = readOptionalString(entry.key);
|
|
166
221
|
const rawText = readOptionalString(entry.rawText);
|
|
167
|
-
|
|
222
|
+
const label = readOptionalString(entry.label);
|
|
223
|
+
if (!kind || !label || !rawText) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (kind === 'skill') {
|
|
227
|
+
const ref = readOptionalString(entry.ref);
|
|
228
|
+
const name = readOptionalString(entry.name);
|
|
229
|
+
const path = readOptionalString(entry.path);
|
|
230
|
+
const source = readSkillSource(entry.source);
|
|
231
|
+
if (!ref || !name || !path || !source) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
tokens.push({ kind, ref, name, source, path, label, rawText });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const key = readOptionalString(entry.key);
|
|
238
|
+
if (!key) {
|
|
168
239
|
continue;
|
|
169
240
|
}
|
|
170
241
|
tokens.push({
|
|
171
242
|
kind,
|
|
172
243
|
key,
|
|
173
244
|
rawText,
|
|
174
|
-
label
|
|
245
|
+
label,
|
|
175
246
|
});
|
|
176
247
|
}
|
|
177
248
|
|
|
178
249
|
return dedupeInlineTokens(tokens);
|
|
179
250
|
}
|
|
251
|
+
|
|
252
|
+
function readSkillSource(value: unknown): ChatSkillSource | null {
|
|
253
|
+
return value === 'builtin' || value === 'global' || value === 'project' || value === 'workspace'
|
|
254
|
+
? value
|
|
255
|
+
: null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function readLegacyInlineTokens(entries: readonly unknown[]): ChatInlineTokenSource[] {
|
|
259
|
+
const tokens: ChatInlineTokenSource[] = [];
|
|
260
|
+
for (const entry of entries) {
|
|
261
|
+
if (!isRecord(entry)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const kind = readOptionalString(entry.kind);
|
|
265
|
+
const key = readOptionalString(entry.key);
|
|
266
|
+
const rawText = readOptionalString(entry.rawText);
|
|
267
|
+
if (!kind || !key || !rawText) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const label = readOptionalString(entry.label) ?? key;
|
|
271
|
+
tokens.push(kind === 'skill'
|
|
272
|
+
? { kind, ref: key, name: label, source: null, path: null, label, rawText }
|
|
273
|
+
: { kind, key, label, rawText });
|
|
274
|
+
}
|
|
275
|
+
return dedupeInlineTokens(tokens);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function createInlineTokensMetadata(
|
|
279
|
+
items: ChatInlineTokenMetadata[],
|
|
280
|
+
): ChatInlineTokensMetadata {
|
|
281
|
+
return {
|
|
282
|
+
schemaVersion: CHAT_INLINE_TOKENS_SCHEMA_VERSION,
|
|
283
|
+
items,
|
|
284
|
+
};
|
|
285
|
+
}
|