@nextclaw/ui 0.15.5 → 0.15.7
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 +41 -0
- package/dist/assets/api-V8m62-13.js +13 -0
- package/dist/assets/app-presenter-provider-Bzj9h_ZS.js +106 -0
- package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DwBwu1AU.js} +1 -1
- package/dist/assets/{book-open-CN7Shpij.js → book-open-DtVSr5O9.js} +1 -1
- package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-CYIqMliJ.js} +2 -2
- package/dist/assets/{chat-page-152YuwfU.js → chat-page-BC0icTtI.js} +1 -1
- package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-B88prd8s.js} +1 -1
- package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-BB09IC2u.js} +1 -1
- package/dist/assets/desktop-update-config-B-q0OhxO.js +1 -0
- package/dist/assets/{dist-D5ycbwCo.js → dist-Bytser-a.js} +1 -1
- package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-B1FXvZm6.js} +1 -1
- package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
- package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-D5VLNoj3.js} +1 -1
- package/dist/assets/{download-CyPgglKr.js → download-CdO6NoHj.js} +1 -1
- package/dist/assets/{external-link-BahW1sl3.js → external-link-C5NFoh5I.js} +1 -1
- package/dist/assets/{eye-D8gGXMkD.js → eye-B51oNRRc.js} +1 -1
- package/dist/assets/{folder-DGWqFDvE.js → folder-DxH7Nef-.js} +1 -1
- package/dist/assets/{form-actions-D2imZLnU.js → form-actions-C9pUwtYV.js} +1 -1
- package/dist/assets/index-DtWf7kk9.css +1 -0
- package/dist/assets/index-UnY4HtVa.js +2 -0
- package/dist/assets/loader-circle-BRSfaztA.js +1 -0
- package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-D2vORX3a.js} +1 -1
- package/dist/assets/mcp-marketplace-page-CGVu7mfL.js +9 -0
- package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
- package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-DYpchGVE.js} +1 -1
- package/dist/assets/plus-BgMBV9HJ.js +1 -0
- package/dist/assets/{popover-Btp7KVc3.js → popover-CONujDHf.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
- package/dist/assets/providers-config-page-DXsoyMrh.js +1 -0
- package/dist/assets/{react-BY-U3h3t.js → react-Io6Pfq8D.js} +1 -1
- package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-C6j2tjul.js} +1 -1
- package/dist/assets/remote-C1y3ARHu.js +1 -0
- package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
- package/dist/assets/runtime-config-page-eYrrKBrA.js +1 -0
- package/dist/assets/{save-DNg2yJTS.js → save-CBBZVvZ-.js} +1 -1
- package/dist/assets/{search-3ykfrAFt.js → search-DFF842z2.js} +1 -1
- package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-DUyuwD7d.js} +1 -1
- package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-DwzKQJ76.js} +2 -2
- package/dist/assets/security-config-D8me3pXl.js +1 -0
- package/dist/assets/{select-B5EecXJt.js → select-BiOpo2kV.js} +1 -1
- package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-C3WQXLtS.js} +1 -1
- package/dist/assets/{settings-2-D-3um9Az.js → settings-2-DyOvA_6K.js} +1 -1
- package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-CQIAEbA4.js} +1 -1
- package/dist/assets/{tabs-BVD8gp3y.js → tabs-BTSENlI2.js} +1 -1
- package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-BG5JIGwY.js} +1 -1
- package/dist/assets/{trash-2-cnsMppRC.js → trash-2-CTIfwpt1.js} +1 -1
- package/dist/assets/use-config-CY4uF2Ou.js +1 -0
- package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
- package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
- package/dist/assets/x-C6cM8wkT.js +1 -0
- package/dist/index.html +30 -29
- package/index.html +1 -0
- package/package.json +8 -8
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +97 -3
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
- package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
- package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +70 -2
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
- package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +82 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +187 -63
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +225 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +6 -1
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +357 -0
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +67 -5
- package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +83 -8
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
- package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
- package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +96 -3
- package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
- package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +82 -128
- package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
- package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
- package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
- package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
- package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
- package/src/features/marketplace/components/marketplace-page.tsx +10 -2
- package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
- package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
- package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
- package/src/features/settings/components/config/provider-form.tsx +0 -4
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
- package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
- package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
- package/src/features/system-status/index.ts +9 -0
- package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
- package/src/index.css +10 -16
- package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
- package/src/shared/components/common/brand-header.tsx +121 -35
- package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
- package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
- package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
- package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
- package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
- package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
- package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
- package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
- package/src/shared/hooks/use-projects.ts +28 -0
- package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
- package/src/shared/hooks/use-server-path-search.ts +40 -0
- package/src/shared/lib/api/index.ts +1 -0
- package/src/shared/lib/api/projects/project.utils.ts +14 -0
- package/src/shared/lib/api/types.ts +27 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +40 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +40 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
- package/dist/assets/api-IlX_CbwU.js +0 -13
- package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
- package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
- package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
- package/dist/assets/index-0av6WO9-.css +0 -1
- package/dist/assets/index-CT0iYlxV.js +0 -2
- package/dist/assets/loader-circle-DtKek-qG.js +0 -1
- package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
- package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
- package/dist/assets/plus-BwgE-Zwf.js +0 -1
- package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
- package/dist/assets/remote-BaVECEZw.js +0 -1
- package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
- package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
- package/dist/assets/security-config-BPzet76z.js +0 -1
- package/dist/assets/use-config-D6yx0lb0.js +0 -1
- package/dist/assets/x-B3o1dWR5.js +0 -1
- package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { ChatInputBarAdapterTexts } from '@/features/chat/types/chat-input-
|
|
|
11
11
|
import {
|
|
12
12
|
buildPanelAppInputSurfaceItems,
|
|
13
13
|
type PanelAppInputSurfaceItemTexts,
|
|
14
|
-
} from './
|
|
14
|
+
} from './context-reference-plugin.utils';
|
|
15
15
|
import {
|
|
16
16
|
resolveInputSurfaceMatchTier,
|
|
17
17
|
scoreInputSurfaceSearchCandidate,
|
|
@@ -69,6 +69,7 @@ function buildSlashCommandItems(params: {
|
|
|
69
69
|
})
|
|
70
70
|
.map(({ command }) => ({
|
|
71
71
|
key: `command:${command.key}`,
|
|
72
|
+
icon: 'command',
|
|
72
73
|
title: command.title,
|
|
73
74
|
subtitle: params.texts.commandSubtitle,
|
|
74
75
|
description: command.description,
|
|
@@ -103,6 +104,7 @@ function buildSlashSkillItems(params: {
|
|
|
103
104
|
[...data.recentSkillValues],
|
|
104
105
|
).map((item) => ({
|
|
105
106
|
...item,
|
|
107
|
+
icon: 'skill',
|
|
106
108
|
hintLabel: skillHintLabel,
|
|
107
109
|
sectionKey: item.sectionKey
|
|
108
110
|
? `${SLASH_SKILL_SECTION_KEY}:${item.sectionKey}`
|
|
@@ -125,6 +127,7 @@ function buildSlashPanelAppItems(params: {
|
|
|
125
127
|
texts: params.itemTexts,
|
|
126
128
|
}).map((item) => ({
|
|
127
129
|
...item,
|
|
130
|
+
icon: 'panel-app',
|
|
128
131
|
hintLabel: params.panelAppHintLabel,
|
|
129
132
|
sectionKey: SLASH_PANEL_APP_SECTION_KEY,
|
|
130
133
|
sectionLabel: params.panelAppSectionLabel,
|
|
@@ -89,6 +89,32 @@ describe('deriveNcpMessagePartsFromComposer', () => {
|
|
|
89
89
|
]);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
it('serializes workspace references without turning them into uploaded file parts', () => {
|
|
93
|
+
expect(
|
|
94
|
+
deriveNcpMessagePartsFromComposer(
|
|
95
|
+
[
|
|
96
|
+
createChatComposerTokenNode({
|
|
97
|
+
tokenKind: 'workspace_file',
|
|
98
|
+
tokenKey: 'src/file name.ts',
|
|
99
|
+
label: 'file name.ts',
|
|
100
|
+
}),
|
|
101
|
+
createChatComposerTextNode(' and '),
|
|
102
|
+
createChatComposerTokenNode({
|
|
103
|
+
tokenKind: 'workspace_directory',
|
|
104
|
+
tokenKey: 'docs/设计',
|
|
105
|
+
label: '设计',
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
[],
|
|
109
|
+
),
|
|
110
|
+
).toEqual([
|
|
111
|
+
{
|
|
112
|
+
type: 'text',
|
|
113
|
+
text: '@file:src%2Ffile%20name.ts and @folder:docs%2F%E8%AE%BE%E8%AE%A1',
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
|
|
92
118
|
it('preserves uploaded attachment references when the attachment has a server uri', () => {
|
|
93
119
|
const parts = deriveNcpMessagePartsFromComposer(
|
|
94
120
|
[
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { createChatComposerTextNode, createChatComposerTokenNode } from '@nextclaw/agent-chat-ui';
|
|
2
2
|
import {
|
|
3
3
|
buildInlineTokensFromTextProtocol,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
buildInlineTokensFromComposer,
|
|
5
|
+
CHAT_INLINE_TOKENS_METADATA_KEY,
|
|
6
6
|
readInlineTokensFromMetadata,
|
|
7
|
-
resolveInlineTokensForText
|
|
7
|
+
resolveInlineTokensForText,
|
|
8
|
+
resolveWorkspaceReferencePath,
|
|
8
9
|
} from '@/features/chat/features/input/utils/chat-inline-token.utils';
|
|
9
10
|
|
|
10
11
|
describe('chat-inline-token utils', () => {
|
|
11
12
|
it('builds ordered inline skill tokens from composer nodes', () => {
|
|
12
13
|
expect(
|
|
13
|
-
|
|
14
|
+
buildInlineTokensFromComposer([
|
|
14
15
|
createChatComposerTextNode('before '),
|
|
15
16
|
createChatComposerTokenNode({
|
|
16
17
|
tokenKind: 'skill',
|
|
@@ -42,7 +43,7 @@ describe('chat-inline-token utils', () => {
|
|
|
42
43
|
it('reads generic inline token metadata safely', () => {
|
|
43
44
|
expect(
|
|
44
45
|
readInlineTokensFromMetadata({
|
|
45
|
-
[
|
|
46
|
+
[CHAT_INLINE_TOKENS_METADATA_KEY]: [
|
|
46
47
|
{
|
|
47
48
|
kind: 'skill',
|
|
48
49
|
key: 'weather',
|
|
@@ -97,4 +98,65 @@ describe('chat-inline-token utils', () => {
|
|
|
97
98
|
}
|
|
98
99
|
]);
|
|
99
100
|
});
|
|
101
|
+
|
|
102
|
+
it('serializes and parses workspace file and directory references', () => {
|
|
103
|
+
expect(
|
|
104
|
+
buildInlineTokensFromComposer([
|
|
105
|
+
createChatComposerTokenNode({
|
|
106
|
+
tokenKind: 'workspace_file',
|
|
107
|
+
tokenKey: 'src/file name.ts',
|
|
108
|
+
label: 'file name.ts',
|
|
109
|
+
}),
|
|
110
|
+
createChatComposerTokenNode({
|
|
111
|
+
tokenKind: 'workspace_directory',
|
|
112
|
+
tokenKey: 'docs/设计',
|
|
113
|
+
label: '设计',
|
|
114
|
+
}),
|
|
115
|
+
]),
|
|
116
|
+
).toEqual([
|
|
117
|
+
{
|
|
118
|
+
kind: 'workspace_file',
|
|
119
|
+
key: 'src/file name.ts',
|
|
120
|
+
label: 'file name.ts',
|
|
121
|
+
rawText: '@file:src%2Ffile%20name.ts',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
kind: 'workspace_directory',
|
|
125
|
+
key: 'docs/设计',
|
|
126
|
+
label: '设计',
|
|
127
|
+
rawText: '@folder:docs%2F%E8%AE%BE%E8%AE%A1',
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
expect(
|
|
131
|
+
buildInlineTokensFromTextProtocol('review @file:src%2Ffile%20name.ts and @folder:docs%2F%E8%AE%BE%E8%AE%A1'),
|
|
132
|
+
).toEqual([
|
|
133
|
+
{
|
|
134
|
+
kind: 'workspace_file',
|
|
135
|
+
key: 'src/file name.ts',
|
|
136
|
+
label: 'file name.ts',
|
|
137
|
+
rawText: '@file:src%2Ffile%20name.ts',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
kind: 'workspace_directory',
|
|
141
|
+
key: 'docs/设计',
|
|
142
|
+
label: '设计',
|
|
143
|
+
rawText: '@folder:docs%2F%E8%AE%BE%E8%AE%A1',
|
|
144
|
+
},
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('resolves workspace token paths inside POSIX and Windows project roots', () => {
|
|
149
|
+
expect(resolveWorkspaceReferencePath({
|
|
150
|
+
projectRoot: '/tmp/project/',
|
|
151
|
+
relativePath: 'docs/guide.md',
|
|
152
|
+
})).toBe('/tmp/project/docs/guide.md');
|
|
153
|
+
expect(resolveWorkspaceReferencePath({
|
|
154
|
+
projectRoot: 'C:\\workspace\\nextclaw\\',
|
|
155
|
+
relativePath: 'docs/guide.md',
|
|
156
|
+
})).toBe('C:\\workspace\\nextclaw\\docs\\guide.md');
|
|
157
|
+
expect(resolveWorkspaceReferencePath({
|
|
158
|
+
projectRoot: '/tmp/project',
|
|
159
|
+
relativePath: '../secret.txt',
|
|
160
|
+
})).toBeNull();
|
|
161
|
+
});
|
|
100
162
|
});
|
|
@@ -10,9 +10,7 @@ import {
|
|
|
10
10
|
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
11
11
|
import type { NcpMessagePart } from '@nextclaw/ncp';
|
|
12
12
|
import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
|
|
13
|
-
|
|
14
|
-
const CHAT_SKILL_TOKEN_PREFIX = '$';
|
|
15
|
-
const CHAT_PANEL_APP_TOKEN_PREFIX = '@panel-app:';
|
|
13
|
+
import { serializeChatComposerTokenText } from './chat-composer-token-protocol.utils';
|
|
16
14
|
|
|
17
15
|
function appendTextPart(parts: NcpMessagePart[], text: string): NcpMessagePart[] {
|
|
18
16
|
if (text.length === 0) {
|
|
@@ -110,13 +108,9 @@ export function deriveNcpMessagePartsFromComposer(
|
|
|
110
108
|
continue;
|
|
111
109
|
}
|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (node.tokenKind === 'panel_app') {
|
|
119
|
-
parts = appendTextPart(parts, `${CHAT_PANEL_APP_TOKEN_PREFIX}${node.tokenKey}`);
|
|
111
|
+
const tokenText = serializeChatComposerTokenText(node);
|
|
112
|
+
if (tokenText) {
|
|
113
|
+
parts = appendTextPart(parts, tokenText);
|
|
120
114
|
continue;
|
|
121
115
|
}
|
|
122
116
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
|
|
3
|
+
CHAT_WORKSPACE_FILE_TOKEN_KIND,
|
|
4
|
+
} from '@nextclaw/shared';
|
|
5
|
+
|
|
6
|
+
const CHAT_SKILL_TOKEN_PREFIX = '$';
|
|
7
|
+
const CHAT_PANEL_APP_TOKEN_PREFIX = '@panel-app:';
|
|
8
|
+
const CHAT_WORKSPACE_FILE_TOKEN_PREFIX = '@file:';
|
|
9
|
+
const CHAT_WORKSPACE_DIRECTORY_TOKEN_PREFIX = '@folder:';
|
|
10
|
+
|
|
11
|
+
export function serializeChatComposerTokenText(params: {
|
|
12
|
+
tokenKey: string;
|
|
13
|
+
tokenKind: string;
|
|
14
|
+
}): string | null {
|
|
15
|
+
const { tokenKey, tokenKind } = params;
|
|
16
|
+
if (tokenKind === 'skill') {
|
|
17
|
+
return `${CHAT_SKILL_TOKEN_PREFIX}${tokenKey}`;
|
|
18
|
+
}
|
|
19
|
+
if (tokenKind === 'panel_app') {
|
|
20
|
+
return `${CHAT_PANEL_APP_TOKEN_PREFIX}${tokenKey}`;
|
|
21
|
+
}
|
|
22
|
+
if (tokenKind === CHAT_WORKSPACE_FILE_TOKEN_KIND) {
|
|
23
|
+
return `${CHAT_WORKSPACE_FILE_TOKEN_PREFIX}${encodeURIComponent(tokenKey)}`;
|
|
24
|
+
}
|
|
25
|
+
if (tokenKind === CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND) {
|
|
26
|
+
return `${CHAT_WORKSPACE_DIRECTORY_TOKEN_PREFIX}${encodeURIComponent(tokenKey)}`;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
2
|
+
import {
|
|
3
|
+
CHAT_INLINE_TOKENS_METADATA_KEY,
|
|
4
|
+
CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
|
|
5
|
+
CHAT_WORKSPACE_FILE_TOKEN_KIND,
|
|
6
|
+
type ChatInlineTokenMetadata,
|
|
7
|
+
} from '@nextclaw/shared';
|
|
8
|
+
import { serializeChatComposerTokenText } from './chat-composer-token-protocol.utils';
|
|
2
9
|
|
|
3
|
-
export
|
|
4
|
-
const CHAT_SKILL_TOKEN_PREFIX = '$';
|
|
10
|
+
export { CHAT_INLINE_TOKENS_METADATA_KEY };
|
|
5
11
|
const CHAT_PANEL_APP_TOKEN_PREFIX = '@panel-app:';
|
|
6
12
|
const CHAT_PANEL_APP_TOKEN_PATTERN = /@panel-app:([A-Za-z0-9_-]+)/g;
|
|
13
|
+
const CHAT_WORKSPACE_FILE_TOKEN_PATTERN = /@file:([^\s]+)/g;
|
|
14
|
+
const CHAT_WORKSPACE_DIRECTORY_TOKEN_PATTERN = /@folder:([^\s]+)/g;
|
|
7
15
|
|
|
8
|
-
export type ChatInlineTokenSource =
|
|
16
|
+
export type ChatInlineTokenSource = ChatInlineTokenMetadata;
|
|
17
|
+
|
|
18
|
+
export function resolveWorkspaceReferencePath(params: {
|
|
19
|
+
projectRoot: string | null | undefined;
|
|
20
|
+
relativePath: string;
|
|
21
|
+
}): string | null {
|
|
22
|
+
const projectRoot = params.projectRoot?.trim().replace(/[\\/]+$/, '') ?? '';
|
|
23
|
+
const relativeSegments = params.relativePath.trim().replace(/\\/g, '/').split('/');
|
|
24
|
+
if (
|
|
25
|
+
!projectRoot ||
|
|
26
|
+
relativeSegments.length === 0 ||
|
|
27
|
+
relativeSegments.some((segment) => !segment || segment === '.' || segment === '..')
|
|
28
|
+
) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const separator = projectRoot.includes('\\') ? '\\' : '/';
|
|
32
|
+
return `${projectRoot}${separator}${relativeSegments.join(separator)}`;
|
|
33
|
+
}
|
|
9
34
|
|
|
10
35
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
11
36
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -33,22 +58,60 @@ function dedupeInlineTokens(tokens: readonly ChatInlineTokenSource[]): ChatInlin
|
|
|
33
58
|
return output;
|
|
34
59
|
}
|
|
35
60
|
|
|
36
|
-
export function
|
|
61
|
+
export function buildInlineTokensFromComposer(nodes: readonly ChatComposerNode[]): ChatInlineTokenSource[] {
|
|
37
62
|
const tokens: ChatInlineTokenSource[] = [];
|
|
38
63
|
for (const node of nodes) {
|
|
39
|
-
if (node.type !== 'token'
|
|
64
|
+
if (node.type !== 'token') {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
node.tokenKind !== 'skill' &&
|
|
69
|
+
node.tokenKind !== CHAT_WORKSPACE_FILE_TOKEN_KIND &&
|
|
70
|
+
node.tokenKind !== CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND
|
|
71
|
+
) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const rawText = serializeChatComposerTokenText(node);
|
|
75
|
+
if (!rawText) {
|
|
40
76
|
continue;
|
|
41
77
|
}
|
|
42
78
|
tokens.push({
|
|
43
|
-
kind:
|
|
79
|
+
kind: node.tokenKind,
|
|
44
80
|
key: node.tokenKey,
|
|
45
81
|
label: node.label,
|
|
46
|
-
rawText
|
|
82
|
+
rawText,
|
|
47
83
|
});
|
|
48
84
|
}
|
|
49
85
|
return dedupeInlineTokens(tokens);
|
|
50
86
|
}
|
|
51
87
|
|
|
88
|
+
function appendWorkspaceTokens(params: {
|
|
89
|
+
kind: typeof CHAT_WORKSPACE_FILE_TOKEN_KIND | typeof CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND;
|
|
90
|
+
pattern: RegExp;
|
|
91
|
+
text: string;
|
|
92
|
+
tokens: ChatInlineTokenSource[];
|
|
93
|
+
}): void {
|
|
94
|
+
const { kind, pattern, text, tokens } = params;
|
|
95
|
+
for (const match of text.matchAll(pattern)) {
|
|
96
|
+
const encodedKey = match[1];
|
|
97
|
+
if (!encodedKey) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
let key: string;
|
|
101
|
+
try {
|
|
102
|
+
key = decodeURIComponent(encodedKey);
|
|
103
|
+
} catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
tokens.push({
|
|
107
|
+
kind,
|
|
108
|
+
key,
|
|
109
|
+
label: key.split('/').filter(Boolean).at(-1) ?? key,
|
|
110
|
+
rawText: match[0],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
52
115
|
export function buildInlineTokensFromTextProtocol(text: string): ChatInlineTokenSource[] {
|
|
53
116
|
const tokens: ChatInlineTokenSource[] = [];
|
|
54
117
|
for (const match of text.matchAll(CHAT_PANEL_APP_TOKEN_PATTERN)) {
|
|
@@ -63,6 +126,18 @@ export function buildInlineTokensFromTextProtocol(text: string): ChatInlineToken
|
|
|
63
126
|
rawText: `${CHAT_PANEL_APP_TOKEN_PREFIX}${key}`
|
|
64
127
|
});
|
|
65
128
|
}
|
|
129
|
+
appendWorkspaceTokens({
|
|
130
|
+
kind: CHAT_WORKSPACE_FILE_TOKEN_KIND,
|
|
131
|
+
pattern: CHAT_WORKSPACE_FILE_TOKEN_PATTERN,
|
|
132
|
+
text,
|
|
133
|
+
tokens,
|
|
134
|
+
});
|
|
135
|
+
appendWorkspaceTokens({
|
|
136
|
+
kind: CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
|
|
137
|
+
pattern: CHAT_WORKSPACE_DIRECTORY_TOKEN_PATTERN,
|
|
138
|
+
text,
|
|
139
|
+
tokens,
|
|
140
|
+
});
|
|
66
141
|
return dedupeInlineTokens(tokens);
|
|
67
142
|
}
|
|
68
143
|
|
|
@@ -76,7 +151,7 @@ export function resolveInlineTokensForText(
|
|
|
76
151
|
export function readInlineTokensFromMetadata(
|
|
77
152
|
metadata: Record<string, unknown> | undefined
|
|
78
153
|
): ChatInlineTokenSource[] {
|
|
79
|
-
const raw = metadata?.[
|
|
154
|
+
const raw = metadata?.[CHAT_INLINE_TOKENS_METADATA_KEY];
|
|
80
155
|
if (!Array.isArray(raw)) {
|
|
81
156
|
return [];
|
|
82
157
|
}
|
|
@@ -278,6 +278,7 @@ export function buildSkillPickerModel(params: {
|
|
|
278
278
|
const recentKeySet = new Set(groupedRecentSkillValues ?? []);
|
|
279
279
|
return {
|
|
280
280
|
title: texts.title,
|
|
281
|
+
allGroupsLabel: texts.allSkillsLabel,
|
|
281
282
|
searchPlaceholder: texts.searchPlaceholder,
|
|
282
283
|
emptyLabel: texts.emptyLabel,
|
|
283
284
|
loadingLabel: texts.loadingLabel,
|
package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, expect, it, vi } from "vitest";
|
|
3
|
+
import { ChatInlineFilePreview } from "@/features/chat/features/message/components/chat-inline-file-preview";
|
|
4
|
+
|
|
5
|
+
const captures = vi.hoisted(() => ({
|
|
6
|
+
filePreviewProps: [] as Array<{
|
|
7
|
+
showBreadcrumbs?: boolean;
|
|
8
|
+
onHtmlContentHeightChange?: (height: number) => void;
|
|
9
|
+
}>,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock(
|
|
13
|
+
"@/features/chat/features/workspace/components/chat-session-workspace-file-preview",
|
|
14
|
+
() => ({
|
|
15
|
+
ChatSessionWorkspaceFilePreview: (props: {
|
|
16
|
+
showBreadcrumbs?: boolean;
|
|
17
|
+
onHtmlContentHeightChange?: (height: number) => void;
|
|
18
|
+
}) => {
|
|
19
|
+
captures.filePreviewProps.push(props);
|
|
20
|
+
return <div data-testid="inline-workspace-file-preview" />;
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
vi.mock("@/shared/lib/i18n", () => ({
|
|
26
|
+
t: (key: string) => key,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
captures.filePreviewProps = [];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("renders adaptive inline HTML without persistent file chrome", () => {
|
|
34
|
+
const onFileOpen = vi.fn();
|
|
35
|
+
const { container } = render(
|
|
36
|
+
<ChatInlineFilePreview
|
|
37
|
+
display={{
|
|
38
|
+
target: {
|
|
39
|
+
type: "file",
|
|
40
|
+
payload: { path: "preview.html", viewer: "rendered" },
|
|
41
|
+
},
|
|
42
|
+
title: "Preview",
|
|
43
|
+
description: "Rendered HTML",
|
|
44
|
+
}}
|
|
45
|
+
parentSessionKey={null}
|
|
46
|
+
sessionProjectRoot={null}
|
|
47
|
+
sessionWorkingDir={null}
|
|
48
|
+
onFileOpen={onFileOpen}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(screen.queryByText("Preview")).toBeNull();
|
|
53
|
+
expect(screen.queryByText("preview.html")).toBeNull();
|
|
54
|
+
expect(screen.queryByText("Rendered HTML")).toBeNull();
|
|
55
|
+
const preview = container.querySelector(
|
|
56
|
+
'[data-chat-inline-file-preview="true"]',
|
|
57
|
+
);
|
|
58
|
+
const actions = container.querySelector(
|
|
59
|
+
'[data-chat-inline-file-actions="true"]',
|
|
60
|
+
);
|
|
61
|
+
const viewport = container.querySelector<HTMLElement>(
|
|
62
|
+
'[data-chat-inline-file-viewport="true"]',
|
|
63
|
+
);
|
|
64
|
+
const iframeSurface = screen.getByTestId("inline-workspace-file-preview");
|
|
65
|
+
expect(preview?.classList.contains("border")).toBe(false);
|
|
66
|
+
expect(preview?.classList.contains("pt-9")).toBe(false);
|
|
67
|
+
expect(actions?.classList.contains("left-1/2")).toBe(true);
|
|
68
|
+
expect(actions?.classList.contains("-translate-x-1/2")).toBe(true);
|
|
69
|
+
expect(actions?.classList.contains("bottom-full")).toBe(true);
|
|
70
|
+
expect(actions?.classList.contains("pb-2")).toBe(true);
|
|
71
|
+
expect(actions?.classList.contains("right-2")).toBe(false);
|
|
72
|
+
expect(actions?.classList.contains("opacity-0")).toBe(true);
|
|
73
|
+
expect(actions?.classList.contains("pointer-events-none")).toBe(true);
|
|
74
|
+
expect(
|
|
75
|
+
actions?.classList.contains("group-hover/inline-html:opacity-100"),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
expect(
|
|
78
|
+
actions?.classList.contains("group-hover/inline-html:pointer-events-auto"),
|
|
79
|
+
).toBe(true);
|
|
80
|
+
expect(
|
|
81
|
+
actions?.classList.contains("group-focus-within/inline-html:opacity-100"),
|
|
82
|
+
).toBe(true);
|
|
83
|
+
expect(
|
|
84
|
+
actions?.classList.contains(
|
|
85
|
+
"group-focus-within/inline-html:pointer-events-auto",
|
|
86
|
+
),
|
|
87
|
+
).toBe(true);
|
|
88
|
+
expect(
|
|
89
|
+
container.querySelector('[data-chat-inline-file-actions-surface="true"]'),
|
|
90
|
+
).toBeTruthy();
|
|
91
|
+
expect(viewport?.classList.contains("rounded-lg")).toBe(true);
|
|
92
|
+
expect(viewport?.classList.contains("h-[240px]")).toBe(true);
|
|
93
|
+
expect(viewport?.classList.contains("max-h-[min(80vh,720px)]")).toBe(true);
|
|
94
|
+
expect(captures.filePreviewProps.at(-1)?.showBreadcrumbs).toBe(false);
|
|
95
|
+
expect(captures.filePreviewProps.at(-1)?.onHtmlContentHeightChange).toEqual(
|
|
96
|
+
expect.any(Function),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
act(() => {
|
|
100
|
+
captures.filePreviewProps.at(-1)?.onHtmlContentHeightChange?.(560);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(viewport?.style.height).toBe("560px");
|
|
104
|
+
expect(screen.getByTestId("inline-workspace-file-preview")).toBe(
|
|
105
|
+
iframeSurface,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
fireEvent.click(screen.getByLabelText("chatPanelCardExpand"));
|
|
109
|
+
fireEvent.click(screen.getByLabelText("chatWorkspaceOpenSource"));
|
|
110
|
+
|
|
111
|
+
expect(onFileOpen).toHaveBeenNthCalledWith(1, {
|
|
112
|
+
path: "preview.html",
|
|
113
|
+
label: "Preview",
|
|
114
|
+
viewMode: "preview",
|
|
115
|
+
previewViewer: "rendered",
|
|
116
|
+
line: undefined,
|
|
117
|
+
column: undefined,
|
|
118
|
+
});
|
|
119
|
+
expect(onFileOpen).toHaveBeenNthCalledWith(2, {
|
|
120
|
+
path: "preview.html",
|
|
121
|
+
label: "Preview",
|
|
122
|
+
viewMode: "preview",
|
|
123
|
+
previewViewer: "source",
|
|
124
|
+
line: undefined,
|
|
125
|
+
column: undefined,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("keeps file chrome for non-HTML inline previews", () => {
|
|
130
|
+
const { container } = render(
|
|
131
|
+
<ChatInlineFilePreview
|
|
132
|
+
display={{
|
|
133
|
+
target: {
|
|
134
|
+
type: "file",
|
|
135
|
+
payload: { path: "notes.md", viewer: "rendered" },
|
|
136
|
+
},
|
|
137
|
+
title: "Notes",
|
|
138
|
+
}}
|
|
139
|
+
parentSessionKey={null}
|
|
140
|
+
sessionProjectRoot={null}
|
|
141
|
+
sessionWorkingDir={null}
|
|
142
|
+
onFileOpen={vi.fn()}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(screen.getByText("Notes")).toBeTruthy();
|
|
147
|
+
expect(screen.getByText("notes.md")).toBeTruthy();
|
|
148
|
+
expect(
|
|
149
|
+
container
|
|
150
|
+
.querySelector('[data-chat-inline-file-preview="true"]')
|
|
151
|
+
?.classList.contains("border"),
|
|
152
|
+
).toBe(true);
|
|
153
|
+
expect(
|
|
154
|
+
container.querySelector('[data-chat-inline-file-actions="true"]'),
|
|
155
|
+
).toBeNull();
|
|
156
|
+
expect(
|
|
157
|
+
captures.filePreviewProps.at(-1)?.onHtmlContentHeightChange,
|
|
158
|
+
).toBeUndefined();
|
|
159
|
+
});
|