@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/api-V8m62-13.js +13 -0
  3. package/dist/assets/app-presenter-provider-Bzj9h_ZS.js +106 -0
  4. package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DwBwu1AU.js} +1 -1
  5. package/dist/assets/{book-open-CN7Shpij.js → book-open-DtVSr5O9.js} +1 -1
  6. package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-CYIqMliJ.js} +2 -2
  7. package/dist/assets/{chat-page-152YuwfU.js → chat-page-BC0icTtI.js} +1 -1
  8. package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-B88prd8s.js} +1 -1
  9. package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-BB09IC2u.js} +1 -1
  10. package/dist/assets/desktop-update-config-B-q0OhxO.js +1 -0
  11. package/dist/assets/{dist-D5ycbwCo.js → dist-Bytser-a.js} +1 -1
  12. package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-B1FXvZm6.js} +1 -1
  13. package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
  14. package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-D5VLNoj3.js} +1 -1
  15. package/dist/assets/{download-CyPgglKr.js → download-CdO6NoHj.js} +1 -1
  16. package/dist/assets/{external-link-BahW1sl3.js → external-link-C5NFoh5I.js} +1 -1
  17. package/dist/assets/{eye-D8gGXMkD.js → eye-B51oNRRc.js} +1 -1
  18. package/dist/assets/{folder-DGWqFDvE.js → folder-DxH7Nef-.js} +1 -1
  19. package/dist/assets/{form-actions-D2imZLnU.js → form-actions-C9pUwtYV.js} +1 -1
  20. package/dist/assets/index-DtWf7kk9.css +1 -0
  21. package/dist/assets/index-UnY4HtVa.js +2 -0
  22. package/dist/assets/loader-circle-BRSfaztA.js +1 -0
  23. package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-D2vORX3a.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-CGVu7mfL.js +9 -0
  25. package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
  26. package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-DYpchGVE.js} +1 -1
  27. package/dist/assets/plus-BgMBV9HJ.js +1 -0
  28. package/dist/assets/{popover-Btp7KVc3.js → popover-CONujDHf.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
  30. package/dist/assets/providers-config-page-DXsoyMrh.js +1 -0
  31. package/dist/assets/{react-BY-U3h3t.js → react-Io6Pfq8D.js} +1 -1
  32. package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-C6j2tjul.js} +1 -1
  33. package/dist/assets/remote-C1y3ARHu.js +1 -0
  34. package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
  35. package/dist/assets/runtime-config-page-eYrrKBrA.js +1 -0
  36. package/dist/assets/{save-DNg2yJTS.js → save-CBBZVvZ-.js} +1 -1
  37. package/dist/assets/{search-3ykfrAFt.js → search-DFF842z2.js} +1 -1
  38. package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-DUyuwD7d.js} +1 -1
  39. package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-DwzKQJ76.js} +2 -2
  40. package/dist/assets/security-config-D8me3pXl.js +1 -0
  41. package/dist/assets/{select-B5EecXJt.js → select-BiOpo2kV.js} +1 -1
  42. package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-C3WQXLtS.js} +1 -1
  43. package/dist/assets/{settings-2-D-3um9Az.js → settings-2-DyOvA_6K.js} +1 -1
  44. package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-CQIAEbA4.js} +1 -1
  45. package/dist/assets/{tabs-BVD8gp3y.js → tabs-BTSENlI2.js} +1 -1
  46. package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-BG5JIGwY.js} +1 -1
  47. package/dist/assets/{trash-2-cnsMppRC.js → trash-2-CTIfwpt1.js} +1 -1
  48. package/dist/assets/use-config-CY4uF2Ou.js +1 -0
  49. package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
  51. package/dist/assets/x-C6cM8wkT.js +1 -0
  52. package/dist/index.html +30 -29
  53. package/index.html +1 -0
  54. package/package.json +8 -8
  55. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +97 -3
  56. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
  58. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
  59. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
  60. package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
  61. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
  62. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +70 -2
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
  65. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
  67. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
  68. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +82 -0
  69. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +187 -63
  70. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +225 -0
  71. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
  72. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +6 -1
  73. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +357 -0
  74. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
  75. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
  76. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +67 -5
  77. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
  78. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
  79. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +83 -8
  80. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
  81. package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
  82. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
  83. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
  84. package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
  85. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  86. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +96 -3
  87. package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
  88. package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
  89. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
  90. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
  91. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
  92. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
  93. package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
  94. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
  95. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +82 -128
  96. package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
  97. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
  98. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
  99. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
  100. package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
  101. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
  102. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
  103. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
  104. package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
  106. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
  107. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
  108. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
  109. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
  110. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  111. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
  112. package/src/features/marketplace/components/marketplace-page.tsx +10 -2
  113. package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
  114. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
  115. package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
  116. package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
  117. package/src/features/settings/components/config/provider-form.tsx +0 -4
  118. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
  119. package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
  120. package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
  121. package/src/features/system-status/index.ts +9 -0
  122. package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
  123. package/src/index.css +10 -16
  124. package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
  125. package/src/shared/components/common/brand-header.tsx +121 -35
  126. package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
  127. package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
  128. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
  129. package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
  130. package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
  131. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
  132. package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
  133. package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
  134. package/src/shared/hooks/use-projects.ts +28 -0
  135. package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
  136. package/src/shared/hooks/use-server-path-search.ts +40 -0
  137. package/src/shared/lib/api/index.ts +1 -0
  138. package/src/shared/lib/api/projects/project.utils.ts +14 -0
  139. package/src/shared/lib/api/types.ts +27 -0
  140. package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
  141. package/src/shared/lib/i18n/locales/en-US/chat.json +40 -2
  142. package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
  143. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
  144. package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
  145. package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
  146. package/src/shared/lib/i18n/locales/zh-CN/chat.json +40 -2
  147. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
  148. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
  149. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
  150. package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
  151. package/dist/assets/api-IlX_CbwU.js +0 -13
  152. package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
  153. package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
  154. package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
  155. package/dist/assets/index-0av6WO9-.css +0 -1
  156. package/dist/assets/index-CT0iYlxV.js +0 -2
  157. package/dist/assets/loader-circle-DtKek-qG.js +0 -1
  158. package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
  159. package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
  160. package/dist/assets/plus-BwgE-Zwf.js +0 -1
  161. package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
  162. package/dist/assets/remote-BaVECEZw.js +0 -1
  163. package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
  164. package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
  165. package/dist/assets/security-config-BPzet76z.js +0 -1
  166. package/dist/assets/use-config-D6yx0lb0.js +0 -1
  167. package/dist/assets/x-B3o1dWR5.js +0 -1
  168. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
  169. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
@@ -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 './panel-app-reference-plugin.utils';
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
- buildInlineSkillTokensFromComposer,
5
- CHAT_UI_INLINE_TOKENS_METADATA_KEY,
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
- buildInlineSkillTokensFromComposer([
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
- [CHAT_UI_INLINE_TOKENS_METADATA_KEY]: [
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
- if (node.tokenKind === 'skill') {
114
- parts = appendTextPart(parts, `${CHAT_SKILL_TOKEN_PREFIX}${node.tokenKey}`);
115
- continue;
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 const CHAT_UI_INLINE_TOKENS_METADATA_KEY = 'ui_inline_tokens';
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 = { kind: string; key: string; label: string; rawText: string };
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 buildInlineSkillTokensFromComposer(nodes: readonly ChatComposerNode[]): ChatInlineTokenSource[] {
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' || node.tokenKind !== 'skill') {
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: 'skill',
79
+ kind: node.tokenKind,
44
80
  key: node.tokenKey,
45
81
  label: node.label,
46
- rawText: `${CHAT_SKILL_TOKEN_PREFIX}${node.tokenKey}`
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?.[CHAT_UI_INLINE_TOKENS_METADATA_KEY];
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,
@@ -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
+ });