@nextclaw/ui 0.15.5 → 0.15.6

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 (147) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/assets/api-Dyb5rgh0.js +13 -0
  3. package/dist/assets/app-presenter-provider-7jV0nPiL.js +106 -0
  4. package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DFm3BbEb.js} +1 -1
  5. package/dist/assets/{book-open-CN7Shpij.js → book-open-L6LCuqOr.js} +1 -1
  6. package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-DOaPKNJA.js} +2 -2
  7. package/dist/assets/{chat-page-152YuwfU.js → chat-page-Cw9uDhD_.js} +1 -1
  8. package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-CqlqjnCK.js} +1 -1
  9. package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-bAp8NU93.js} +1 -1
  10. package/dist/assets/desktop-update-config-2SjMfCMl.js +1 -0
  11. package/dist/assets/{dist-D5ycbwCo.js → dist-CrtbGLlh.js} +1 -1
  12. package/dist/assets/doc-browser-A5IRyDXQ2.js +1 -0
  13. package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-BmnQa30-.js} +1 -1
  14. package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-BlpUUFrR.js} +1 -1
  15. package/dist/assets/{download-CyPgglKr.js → download-BhZZNXD8.js} +1 -1
  16. package/dist/assets/{external-link-BahW1sl3.js → external-link-B9Uoq5J7.js} +1 -1
  17. package/dist/assets/{eye-D8gGXMkD.js → eye-K5knauEM.js} +1 -1
  18. package/dist/assets/{folder-DGWqFDvE.js → folder-BIYZZWJv.js} +1 -1
  19. package/dist/assets/{form-actions-D2imZLnU.js → form-actions-CKprWFeM.js} +1 -1
  20. package/dist/assets/index-BVn702m4.css +1 -0
  21. package/dist/assets/{index-CT0iYlxV.js → index-CFxJ1Fkg.js} +2 -2
  22. package/dist/assets/loader-circle-Bj2RMrCR.js +1 -0
  23. package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-BQ-XFZod.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-BfA7A3n4.js +9 -0
  25. package/dist/assets/mcp-marketplace-page-CXHYO95j.js +1 -0
  26. package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-BbWbW_qM.js} +1 -1
  27. package/dist/assets/plus-TmFdwO3P.js +1 -0
  28. package/dist/assets/{popover-Btp7KVc3.js → popover-CX9zFsuc.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-B-9_DtIP.js} +1 -1
  30. package/dist/assets/providers-config-page-xwIjGur2.js +1 -0
  31. package/dist/assets/{react-BY-U3h3t.js → react-DsgudwpT.js} +1 -1
  32. package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-kXdvbhFf.js} +1 -1
  33. package/dist/assets/remote-B9F2uCk9.js +1 -0
  34. package/dist/assets/rotate-cw-DEc6aihr.js +1 -0
  35. package/dist/assets/runtime-config-page-DTsi7czJ.js +1 -0
  36. package/dist/assets/{save-DNg2yJTS.js → save-C9qpz2_2.js} +1 -1
  37. package/dist/assets/{search-3ykfrAFt.js → search-_kEi29qo.js} +1 -1
  38. package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-H7ug8f4D.js} +1 -1
  39. package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-C1_6rINu.js} +2 -2
  40. package/dist/assets/security-config-Cr2ERKLu.js +1 -0
  41. package/dist/assets/{select-B5EecXJt.js → select-MJKhKAWR.js} +1 -1
  42. package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-DA3tJuew.js} +1 -1
  43. package/dist/assets/{settings-2-D-3um9Az.js → settings-2-D_y7zXJz.js} +1 -1
  44. package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-0uTLUhm9.js} +1 -1
  45. package/dist/assets/{tabs-BVD8gp3y.js → tabs-h44XPocd.js} +1 -1
  46. package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-bRZ04q3C.js} +1 -1
  47. package/dist/assets/{trash-2-cnsMppRC.js → trash-2-BRpSE-Wt.js} +1 -1
  48. package/dist/assets/use-config-CwDEAl6f.js +1 -0
  49. package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-BbnWw-8c.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-CqgIMDQL.js} +1 -1
  51. package/dist/assets/x-DdEDL6ju.js +1 -0
  52. package/dist/index.html +29 -29
  53. package/package.json +8 -8
  54. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +84 -1
  55. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
  59. package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
  60. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
  61. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +52 -2
  62. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
  63. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
  64. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
  67. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +37 -0
  68. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +140 -63
  69. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +175 -0
  70. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
  71. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +8 -1
  72. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +274 -0
  73. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
  74. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
  75. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +50 -4
  76. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
  77. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
  78. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +66 -8
  79. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
  80. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +41 -3
  81. package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
  82. package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
  83. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
  84. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
  85. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
  86. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
  87. package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
  88. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
  89. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +81 -127
  90. package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
  91. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
  92. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
  93. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
  94. package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
  95. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
  96. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
  97. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
  98. package/src/features/settings/components/config/provider-form.tsx +0 -4
  99. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
  100. package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
  101. package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
  102. package/src/features/system-status/index.ts +9 -0
  103. package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
  104. package/src/index.css +10 -16
  105. package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
  106. package/src/shared/components/common/brand-header.tsx +121 -35
  107. package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
  108. package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
  109. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
  110. package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
  111. package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
  112. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
  113. package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
  114. package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
  115. package/src/shared/hooks/use-projects.ts +28 -0
  116. package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
  117. package/src/shared/hooks/use-server-path-search.ts +40 -0
  118. package/src/shared/lib/api/index.ts +1 -0
  119. package/src/shared/lib/api/projects/project.utils.ts +14 -0
  120. package/src/shared/lib/api/types.ts +27 -0
  121. package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
  122. package/src/shared/lib/i18n/locales/en-US/chat.json +35 -2
  123. package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
  124. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +4 -0
  125. package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
  126. package/src/shared/lib/i18n/locales/zh-CN/chat.json +35 -2
  127. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
  128. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +4 -0
  129. package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
  130. package/dist/assets/api-IlX_CbwU.js +0 -13
  131. package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
  132. package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
  133. package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
  134. package/dist/assets/index-0av6WO9-.css +0 -1
  135. package/dist/assets/loader-circle-DtKek-qG.js +0 -1
  136. package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
  137. package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
  138. package/dist/assets/plus-BwgE-Zwf.js +0 -1
  139. package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
  140. package/dist/assets/remote-BaVECEZw.js +0 -1
  141. package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
  142. package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
  143. package/dist/assets/security-config-BPzet76z.js +0 -1
  144. package/dist/assets/use-config-D6yx0lb0.js +0 -1
  145. package/dist/assets/x-B3o1dWR5.js +0 -1
  146. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
  147. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
@@ -86,6 +86,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
86
86
  const state = resolveChatInputSurfaceState({
87
87
  data: {
88
88
  isPanelAppsLoading: false,
89
+ isServerPathSearchLoading: false,
89
90
  isSkillsLoading: false,
90
91
  panelApps: [
91
92
  createPanelApp({
@@ -94,7 +95,11 @@ describe('createSlashCommandInputSurfacePlugin', () => {
94
95
  description: 'Track tasks',
95
96
  }),
96
97
  ],
98
+ projectRoot: '/tmp/project',
97
99
  recentSkillValues: [],
100
+ referenceMode: 'root',
101
+ serverPathEntries: [],
102
+ serverPathSearchError: null,
98
103
  skillRecords: [
99
104
  {
100
105
  key: 'web-search',
@@ -125,16 +130,19 @@ describe('createSlashCommandInputSurfacePlugin', () => {
125
130
  { key: 'panel-apps', label: 'Panel Apps', sectionKeys: ['panel-apps'] },
126
131
  ]);
127
132
  expect(state.panel?.items[0]).toMatchObject({
133
+ icon: 'command',
128
134
  sectionKey: 'commands',
129
135
  sectionLabel: 'Commands',
130
136
  hintLabel: 'Run command',
131
137
  });
132
138
  expect(state.panel?.items[1]).toMatchObject({
139
+ icon: 'skill',
133
140
  sectionKey: 'skills',
134
141
  sectionLabel: 'Skills',
135
142
  hintLabel: 'Add skill',
136
143
  });
137
144
  expect(state.panel?.items[2]).toMatchObject({
145
+ icon: 'panel-app',
138
146
  sectionKey: 'panel-apps',
139
147
  sectionLabel: 'Panel Apps',
140
148
  hintLabel: 'Open panel app',
@@ -145,9 +153,14 @@ describe('createSlashCommandInputSurfacePlugin', () => {
145
153
  const state = resolveChatInputSurfaceState({
146
154
  data: {
147
155
  isPanelAppsLoading: false,
156
+ isServerPathSearchLoading: false,
148
157
  isSkillsLoading: false,
149
158
  panelApps: [],
159
+ projectRoot: '/tmp/project',
150
160
  recentSkillValues: [],
161
+ referenceMode: 'root',
162
+ serverPathEntries: [],
163
+ serverPathSearchError: null,
151
164
  skillRecords: [
152
165
  {
153
166
  key: 'project:review',
@@ -189,9 +202,14 @@ describe('createSlashCommandInputSurfacePlugin', () => {
189
202
  const state = resolveChatInputSurfaceState({
190
203
  data: {
191
204
  isPanelAppsLoading: false,
205
+ isServerPathSearchLoading: false,
192
206
  isSkillsLoading: false,
193
207
  panelApps: [],
208
+ projectRoot: '/tmp/project',
194
209
  recentSkillValues: [],
210
+ referenceMode: 'root',
211
+ serverPathEntries: [],
212
+ serverPathSearchError: null,
195
213
  skillRecords: [],
196
214
  },
197
215
  plugins: [createPlugin({ onSelectCommand })],
@@ -215,6 +233,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
215
233
  const state = resolveChatInputSurfaceState({
216
234
  data: {
217
235
  isPanelAppsLoading: false,
236
+ isServerPathSearchLoading: false,
218
237
  isSkillsLoading: false,
219
238
  panelApps: [
220
239
  createPanelApp({
@@ -223,7 +242,11 @@ describe('createSlashCommandInputSurfacePlugin', () => {
223
242
  description: 'Track tasks',
224
243
  }),
225
244
  ],
245
+ projectRoot: '/tmp/project',
226
246
  recentSkillValues: [],
247
+ referenceMode: 'root',
248
+ serverPathEntries: [],
249
+ serverPathSearchError: null,
227
250
  skillRecords: [],
228
251
  },
229
252
  plugins: [createPlugin({ onSelectPanelApp })],
@@ -1,10 +1,17 @@
1
1
  import type { ChatSkillRecord } from '@/features/chat/types/chat-input-bar.types';
2
- import type { PanelAppEntryView } from '@/shared/lib/api';
2
+ import type { PanelAppEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
3
+
4
+ export type ContextReferenceMode = 'root' | 'files';
3
5
 
4
6
  export type ChatInputProductPluginData = {
5
7
  isPanelAppsLoading: boolean;
8
+ isServerPathSearchLoading: boolean;
6
9
  isSkillsLoading: boolean;
7
10
  panelApps: readonly PanelAppEntryView[];
11
+ projectRoot: string;
8
12
  recentSkillValues: readonly string[];
13
+ referenceMode: ContextReferenceMode;
14
+ serverPathEntries: readonly ServerPathSearchEntryView[];
15
+ serverPathSearchError: string | null;
9
16
  skillRecords: readonly ChatSkillRecord[];
10
17
  };
@@ -0,0 +1,274 @@
1
+ import {
2
+ createInputSurfaceTriggeredPanelPlugin,
3
+ type ChatInputSurfaceItem,
4
+ type ChatInputSurfaceMenuTexts,
5
+ type ChatInputSurfacePlugin,
6
+ type ChatInputSurfaceTriggerSpec,
7
+ } from '@nextclaw/agent-chat-ui';
8
+ import {
9
+ CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
10
+ CHAT_WORKSPACE_FILE_TOKEN_KIND,
11
+ } from '@nextclaw/shared';
12
+ import type { PanelAppEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
13
+ import type {
14
+ ChatInputProductPluginData,
15
+ ContextReferenceMode,
16
+ } from './chat-input-product-plugin-adapters.types';
17
+ import {
18
+ scoreInputSurfaceSearchCandidate,
19
+ resolveInputSurfaceMatchTier,
20
+ } from './input-surface-search.utils';
21
+
22
+ const FILES_NAVIGATION_ITEM_KEY = 'context-reference:navigate:files';
23
+ const ROOT_NAVIGATION_ITEM_KEY = 'context-reference:navigate:root';
24
+ const WORKSPACE_SECTION_KEY = 'workspace';
25
+ const PANEL_APP_SECTION_KEY = 'panel-apps';
26
+
27
+ export const CONTEXT_REFERENCE_TRIGGER_SPEC: ChatInputSurfaceTriggerSpec = {
28
+ key: 'context-reference',
29
+ marker: '@',
30
+ };
31
+
32
+ export type PanelAppInputSurfaceItemTexts = {
33
+ appIdLabel: string;
34
+ fileLabel: string;
35
+ noDescriptionLabel: string;
36
+ subtitle: string;
37
+ };
38
+
39
+ export type ContextReferenceInputSurfaceTexts = {
40
+ backLabel: string;
41
+ backDescription: string;
42
+ backHintLabel: string;
43
+ directoryDescription: string;
44
+ fileDescription: string;
45
+ filesDescription: string;
46
+ filesHintLabel: string;
47
+ filesLabel: string;
48
+ filesSubtitle: string;
49
+ panelAppSectionLabel: string;
50
+ projectRootLabel: string;
51
+ searchFailedLabel: string;
52
+ workspaceSectionLabel: string;
53
+ };
54
+
55
+ function getPanelAppActivityTime(entry: PanelAppEntryView): number {
56
+ return Date.parse(entry.lastOpenedAt ?? entry.updatedAt) || 0;
57
+ }
58
+
59
+ function resolveProjectLabel(projectRoot: string): string {
60
+ return projectRoot.split(/[\\/]+/).filter(Boolean).at(-1) ?? projectRoot;
61
+ }
62
+
63
+ function resolvePanelAppInputSurfaceEntries(params: {
64
+ entries: readonly PanelAppEntryView[];
65
+ query: string;
66
+ }): PanelAppEntryView[] {
67
+ const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
68
+ return params.entries
69
+ .map((entry, order) => ({
70
+ entry,
71
+ order,
72
+ score: scoreInputSurfaceSearchCandidate(
73
+ {
74
+ id: entry.appId,
75
+ label: entry.title || entry.appId,
76
+ description: entry.description,
77
+ aliases: [entry.id, entry.fileName],
78
+ },
79
+ params.query,
80
+ ),
81
+ }))
82
+ .filter((entry) => entry.score > 0)
83
+ .sort((left, right) => {
84
+ const leftTier = resolveInputSurfaceMatchTier(left.score);
85
+ const rightTier = resolveInputSurfaceMatchTier(right.score);
86
+ if (rightTier !== leftTier) {
87
+ return rightTier - leftTier;
88
+ }
89
+ if (left.entry.favorite !== right.entry.favorite) {
90
+ return left.entry.favorite ? -1 : 1;
91
+ }
92
+ const rightActivity = getPanelAppActivityTime(right.entry);
93
+ const leftActivity = getPanelAppActivityTime(left.entry);
94
+ if (rightActivity !== leftActivity) {
95
+ return rightActivity - leftActivity;
96
+ }
97
+ if (right.score !== left.score) {
98
+ return right.score - left.score;
99
+ }
100
+ const labelCompare = collator.compare(left.entry.title || left.entry.appId, right.entry.title || right.entry.appId);
101
+ return labelCompare || left.order - right.order;
102
+ })
103
+ .map(({ entry }) => entry);
104
+ }
105
+
106
+ function buildPanelAppInputSurfaceItemEntries(params: {
107
+ entries: readonly PanelAppEntryView[];
108
+ keyPrefix?: string;
109
+ query: string;
110
+ texts: PanelAppInputSurfaceItemTexts;
111
+ }): Array<{ entry: PanelAppEntryView; item: ChatInputSurfaceItem }> {
112
+ const keyPrefix = params.keyPrefix ?? 'panel-app';
113
+ return resolvePanelAppInputSurfaceEntries({
114
+ entries: params.entries,
115
+ query: params.query,
116
+ }).map((entry) => ({
117
+ entry,
118
+ item: {
119
+ key: `${keyPrefix}:${entry.appId}`,
120
+ icon: 'panel-app',
121
+ title: entry.title || entry.appId,
122
+ subtitle: params.texts.subtitle,
123
+ description: (entry.description ?? '').trim() || params.texts.noDescriptionLabel,
124
+ detailLines: [
125
+ `${params.texts.appIdLabel}: ${entry.appId}`,
126
+ `${params.texts.fileLabel}: ${entry.fileName}`,
127
+ ],
128
+ },
129
+ }));
130
+ }
131
+
132
+ export function buildPanelAppInputSurfaceItems(params: {
133
+ entries: readonly PanelAppEntryView[];
134
+ keyPrefix?: string;
135
+ query: string;
136
+ texts: PanelAppInputSurfaceItemTexts;
137
+ }): ChatInputSurfaceItem[] {
138
+ return buildPanelAppInputSurfaceItemEntries(params).map(({ item }) => item);
139
+ }
140
+
141
+ function buildPanelAppReferenceItems(params: {
142
+ entries: readonly PanelAppEntryView[];
143
+ query: string;
144
+ sectionLabel: string;
145
+ texts: PanelAppInputSurfaceItemTexts;
146
+ }): ChatInputSurfaceItem[] {
147
+ return buildPanelAppInputSurfaceItemEntries(params).map(({ entry, item }) => ({
148
+ ...item,
149
+ sectionKey: PANEL_APP_SECTION_KEY,
150
+ sectionLabel: params.sectionLabel,
151
+ tokenKind: 'panel_app',
152
+ tokenKey: entry.appId,
153
+ value: entry.appId,
154
+ }));
155
+ }
156
+
157
+ function buildWorkspaceReferenceItems(params: {
158
+ entries: readonly ServerPathSearchEntryView[];
159
+ projectRoot: string;
160
+ texts: ContextReferenceInputSurfaceTexts;
161
+ }): ChatInputSurfaceItem[] {
162
+ const projectLabel = resolveProjectLabel(params.projectRoot);
163
+ return params.entries.map((entry) => {
164
+ const pathSegments = entry.relativePath.split('/').filter(Boolean);
165
+ return {
166
+ key: `workspace:${entry.kind}:${entry.relativePath}`,
167
+ icon: entry.kind === 'directory' ? 'folder' : 'file',
168
+ title: entry.name,
169
+ subtitle: entry.parentRelativePath || projectLabel,
170
+ description: entry.kind === 'directory'
171
+ ? params.texts.directoryDescription
172
+ : params.texts.fileDescription,
173
+ detailLines: [
174
+ `${params.texts.projectRootLabel}: ${params.projectRoot}`,
175
+ entry.relativePath,
176
+ ],
177
+ sectionKey: WORKSPACE_SECTION_KEY,
178
+ sectionLabel: params.texts.workspaceSectionLabel,
179
+ tokenKind: entry.kind === 'directory'
180
+ ? CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND
181
+ : CHAT_WORKSPACE_FILE_TOKEN_KIND,
182
+ tokenKey: entry.relativePath,
183
+ pathPreview: {
184
+ rootLabel: projectLabel,
185
+ segments: pathSegments.map((label, index) => ({
186
+ label,
187
+ kind: index === pathSegments.length - 1 ? entry.kind : 'directory',
188
+ })),
189
+ },
190
+ } satisfies ChatInputSurfaceItem;
191
+ });
192
+ }
193
+
194
+ function buildFilesNavigationItem(texts: ContextReferenceInputSurfaceTexts): ChatInputSurfaceItem {
195
+ return {
196
+ key: FILES_NAVIGATION_ITEM_KEY,
197
+ icon: 'files',
198
+ title: texts.filesLabel,
199
+ subtitle: texts.filesSubtitle,
200
+ description: texts.filesDescription,
201
+ detailLines: [],
202
+ hintLabel: texts.filesHintLabel,
203
+ selectionBehavior: 'navigate',
204
+ };
205
+ }
206
+
207
+ function buildRootNavigationItem(texts: ContextReferenceInputSurfaceTexts): ChatInputSurfaceItem {
208
+ return {
209
+ key: ROOT_NAVIGATION_ITEM_KEY,
210
+ icon: 'back',
211
+ title: texts.backLabel,
212
+ subtitle: '',
213
+ description: texts.backDescription,
214
+ detailLines: [],
215
+ hintLabel: texts.backHintLabel,
216
+ selectionBehavior: 'navigate',
217
+ };
218
+ }
219
+
220
+ export function createContextReferenceInputSurfacePlugin(params: {
221
+ itemTexts: {
222
+ context: ContextReferenceInputSurfaceTexts;
223
+ panelApp: PanelAppInputSurfaceItemTexts;
224
+ };
225
+ menuTexts: ChatInputSurfaceMenuTexts;
226
+ onNavigate: (mode: ContextReferenceMode) => void;
227
+ }): ChatInputSurfacePlugin<ChatInputProductPluginData> {
228
+ return createInputSurfaceTriggeredPanelPlugin({
229
+ key: 'context-reference',
230
+ trigger: CONTEXT_REFERENCE_TRIGGER_SPEC,
231
+ resolvePanel: ({ data, trigger }) => {
232
+ const workspaceItems = buildWorkspaceReferenceItems({
233
+ entries: data.serverPathEntries,
234
+ projectRoot: data.projectRoot,
235
+ texts: params.itemTexts.context,
236
+ });
237
+ const items = data.referenceMode === 'files'
238
+ ? [buildRootNavigationItem(params.itemTexts.context), ...workspaceItems]
239
+ : [
240
+ buildFilesNavigationItem(params.itemTexts.context),
241
+ ...(trigger.query ? workspaceItems : []),
242
+ ...buildPanelAppReferenceItems({
243
+ entries: data.panelApps,
244
+ query: trigger.query,
245
+ sectionLabel: params.itemTexts.context.panelAppSectionLabel,
246
+ texts: params.itemTexts.panelApp,
247
+ }),
248
+ ];
249
+ const relevantLoading = data.referenceMode === 'files'
250
+ ? data.isServerPathSearchLoading
251
+ : data.isPanelAppsLoading || (Boolean(trigger.query) && data.isServerPathSearchLoading);
252
+ return {
253
+ isLoading: data.referenceMode === 'files'
254
+ ? relevantLoading
255
+ : relevantLoading && items.length === 0,
256
+ items,
257
+ notice: data.serverPathSearchError
258
+ ? {
259
+ message: `${params.itemTexts.context.searchFailedLabel}: ${data.serverPathSearchError}`,
260
+ tone: 'error',
261
+ }
262
+ : undefined,
263
+ onSelectItem: (item) => {
264
+ if (item.key === FILES_NAVIGATION_ITEM_KEY) {
265
+ params.onNavigate('files');
266
+ } else if (item.key === ROOT_NAVIGATION_ITEM_KEY) {
267
+ params.onNavigate('root');
268
+ }
269
+ },
270
+ texts: params.menuTexts,
271
+ };
272
+ },
273
+ });
274
+ }
@@ -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,8 +1,8 @@
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
7
  resolveInlineTokensForText
8
8
  } from '@/features/chat/features/input/utils/chat-inline-token.utils';
@@ -10,7 +10,7 @@ import {
10
10
  describe('chat-inline-token utils', () => {
11
11
  it('builds ordered inline skill tokens from composer nodes', () => {
12
12
  expect(
13
- buildInlineSkillTokensFromComposer([
13
+ buildInlineTokensFromComposer([
14
14
  createChatComposerTextNode('before '),
15
15
  createChatComposerTokenNode({
16
16
  tokenKind: 'skill',
@@ -42,7 +42,7 @@ describe('chat-inline-token utils', () => {
42
42
  it('reads generic inline token metadata safely', () => {
43
43
  expect(
44
44
  readInlineTokensFromMetadata({
45
- [CHAT_UI_INLINE_TOKENS_METADATA_KEY]: [
45
+ [CHAT_INLINE_TOKENS_METADATA_KEY]: [
46
46
  {
47
47
  kind: 'skill',
48
48
  key: 'weather',
@@ -97,4 +97,50 @@ describe('chat-inline-token utils', () => {
97
97
  }
98
98
  ]);
99
99
  });
100
+
101
+ it('serializes and parses workspace file and directory references', () => {
102
+ expect(
103
+ buildInlineTokensFromComposer([
104
+ createChatComposerTokenNode({
105
+ tokenKind: 'workspace_file',
106
+ tokenKey: 'src/file name.ts',
107
+ label: 'file name.ts',
108
+ }),
109
+ createChatComposerTokenNode({
110
+ tokenKind: 'workspace_directory',
111
+ tokenKey: 'docs/设计',
112
+ label: '设计',
113
+ }),
114
+ ]),
115
+ ).toEqual([
116
+ {
117
+ kind: 'workspace_file',
118
+ key: 'src/file name.ts',
119
+ label: 'file name.ts',
120
+ rawText: '@file:src%2Ffile%20name.ts',
121
+ },
122
+ {
123
+ kind: 'workspace_directory',
124
+ key: 'docs/设计',
125
+ label: '设计',
126
+ rawText: '@folder:docs%2F%E8%AE%BE%E8%AE%A1',
127
+ },
128
+ ]);
129
+ expect(
130
+ buildInlineTokensFromTextProtocol('review @file:src%2Ffile%20name.ts and @folder:docs%2F%E8%AE%BE%E8%AE%A1'),
131
+ ).toEqual([
132
+ {
133
+ kind: 'workspace_file',
134
+ key: 'src/file name.ts',
135
+ label: 'file name.ts',
136
+ rawText: '@file:src%2Ffile%20name.ts',
137
+ },
138
+ {
139
+ kind: 'workspace_directory',
140
+ key: 'docs/设计',
141
+ label: '设计',
142
+ rawText: '@folder:docs%2F%E8%AE%BE%E8%AE%A1',
143
+ },
144
+ ]);
145
+ });
100
146
  });
@@ -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
+ }