@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
@@ -0,0 +1,225 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { resolveChatInputSurfaceState } from '@nextclaw/agent-chat-ui';
3
+ import {
4
+ CONTEXT_REFERENCE_TRIGGER_SPEC,
5
+ createContextReferenceInputSurfacePlugin,
6
+ } from '@/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils';
7
+ import type { PanelAppEntryView } from '@/shared/lib/api';
8
+
9
+ function createPanelApp(): PanelAppEntryView {
10
+ return {
11
+ id: 'task-board',
12
+ appId: 'task-board',
13
+ fileName: 'task-board.panel.html',
14
+ kind: 'single-file',
15
+ title: 'Task Board',
16
+ description: 'Track tasks',
17
+ contentPath: '/panels/task-board.panel.html',
18
+ createdAt: '2026-06-18T00:00:00.000Z',
19
+ updatedAt: '2026-06-18T00:00:00.000Z',
20
+ sizeBytes: 100,
21
+ favorite: false,
22
+ clientDeclared: false,
23
+ clientGranted: false,
24
+ openCount: 0,
25
+ };
26
+ }
27
+
28
+ function createPlugin(onNavigate = vi.fn()) {
29
+ return createContextReferenceInputSurfacePlugin({
30
+ itemTexts: {
31
+ context: {
32
+ backLabel: 'Back',
33
+ backDescription: 'Back to references',
34
+ backHintLabel: 'Enter to go back',
35
+ currentDirectoryLabel: 'Current folder',
36
+ directoryDescription: 'Directory context',
37
+ fileDescription: 'File context',
38
+ filesDescription: 'Browse project files',
39
+ filesHintLabel: 'Enter to browse',
40
+ filesLabel: 'Files & Folders',
41
+ filesSubtitle: 'Project Context',
42
+ panelAppSectionLabel: 'Panel Apps',
43
+ parentLabel: 'Up one folder',
44
+ parentDescription: 'Browse parent',
45
+ parentHintLabel: 'Enter to browse parent',
46
+ projectRootLabel: 'Project directory',
47
+ searchFailedLabel: 'Search failed',
48
+ workspaceSectionLabel: 'Files & Folders',
49
+ },
50
+ panelApp: {
51
+ appIdLabel: 'App ID',
52
+ fileLabel: 'File',
53
+ noDescriptionLabel: 'No description',
54
+ subtitle: 'Panel App',
55
+ },
56
+ },
57
+ menuTexts: {
58
+ loadingLabel: 'Loading',
59
+ sectionLabel: 'References',
60
+ emptyLabel: 'Empty',
61
+ hintLabel: 'Type @',
62
+ itemHintLabel: 'Enter to reference',
63
+ },
64
+ onNavigate,
65
+ });
66
+ }
67
+
68
+ function createData(referencePath: string | null = null) {
69
+ return {
70
+ isPanelAppsLoading: false,
71
+ isServerPathSearchLoading: false,
72
+ isSkillsLoading: false,
73
+ panelApps: [createPanelApp()],
74
+ projectRoot: '/tmp/project',
75
+ recentSkillValues: [],
76
+ referencePath,
77
+ serverPathEntries: [
78
+ {
79
+ name: 'server-path.ts',
80
+ path: '/tmp/project/src/server-path.ts',
81
+ relativePath: 'src/server-path.ts',
82
+ parentRelativePath: 'src',
83
+ kind: 'file' as const,
84
+ hidden: false,
85
+ },
86
+ {
87
+ name: 'docs',
88
+ path: '/tmp/project/docs',
89
+ relativePath: 'docs',
90
+ parentRelativePath: '',
91
+ kind: 'directory' as const,
92
+ hidden: false,
93
+ },
94
+ ],
95
+ serverPathSearchError: null,
96
+ skillRecords: [],
97
+ };
98
+ }
99
+
100
+ describe('context reference input surface plugin', () => {
101
+ it('shows Files & Folders and panel apps in the root @ menu', () => {
102
+ const state = resolveChatInputSurfaceState({
103
+ plugins: [createPlugin()],
104
+ trigger: {
105
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
106
+ query: '',
107
+ start: 0,
108
+ end: 1,
109
+ },
110
+ data: createData(),
111
+ });
112
+
113
+ expect(state.panel?.items).toEqual([
114
+ expect.objectContaining({
115
+ title: 'Files & Folders',
116
+ icon: 'files',
117
+ selectionBehavior: 'navigate',
118
+ }),
119
+ expect.objectContaining({
120
+ key: 'panel-app:task-board',
121
+ tokenKind: 'panel_app',
122
+ tokenKey: 'task-board',
123
+ }),
124
+ ]);
125
+ });
126
+
127
+ it('builds file and directory tokens with path previews in files mode', () => {
128
+ const state = resolveChatInputSurfaceState({
129
+ plugins: [createPlugin()],
130
+ trigger: {
131
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
132
+ query: 'server',
133
+ start: 0,
134
+ end: 7,
135
+ },
136
+ data: createData(''),
137
+ });
138
+
139
+ expect(state.panel?.items.slice(1)).toEqual([
140
+ expect.objectContaining({
141
+ title: 'server-path.ts',
142
+ tokenKind: 'workspace_file',
143
+ tokenKey: 'src/server-path.ts',
144
+ pathPreview: {
145
+ rootLabel: 'project',
146
+ segments: [
147
+ { label: 'src', kind: 'directory' },
148
+ { label: 'server-path.ts', kind: 'file' },
149
+ ],
150
+ },
151
+ }),
152
+ expect.objectContaining({
153
+ title: 'docs',
154
+ tokenKind: 'workspace_directory',
155
+ tokenKey: 'docs',
156
+ }),
157
+ ]);
158
+ });
159
+
160
+ it('navigates without producing a composer token', () => {
161
+ const onNavigate = vi.fn();
162
+ const state = resolveChatInputSurfaceState({
163
+ plugins: [createPlugin(onNavigate)],
164
+ trigger: {
165
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
166
+ query: '',
167
+ start: 0,
168
+ end: 1,
169
+ },
170
+ data: createData(),
171
+ });
172
+ const item = state.panel?.items[0];
173
+
174
+ state.panel?.onSelectItem?.(item!);
175
+
176
+ expect(onNavigate).toHaveBeenCalledWith('');
177
+ expect(item).not.toHaveProperty('tokenKind');
178
+ });
179
+
180
+ it('opens browsed directories and offers the current folder as a token', () => {
181
+ const onNavigate = vi.fn();
182
+ const rootState = resolveChatInputSurfaceState({
183
+ plugins: [createPlugin(onNavigate)],
184
+ trigger: {
185
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
186
+ query: '',
187
+ start: 0,
188
+ end: 1,
189
+ },
190
+ data: createData(''),
191
+ });
192
+ const directoryItem = rootState.panel?.items.find((item) => item.title === 'docs');
193
+
194
+ expect(directoryItem).toMatchObject({
195
+ selectionBehavior: 'navigate',
196
+ value: 'docs',
197
+ });
198
+ expect(directoryItem?.tokenKind).toBeUndefined();
199
+ rootState.panel?.onSelectItem?.(directoryItem!);
200
+ expect(onNavigate).toHaveBeenCalledWith('docs');
201
+
202
+ const nestedState = resolveChatInputSurfaceState({
203
+ plugins: [createPlugin(onNavigate)],
204
+ trigger: {
205
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
206
+ query: '',
207
+ start: 0,
208
+ end: 1,
209
+ },
210
+ data: createData('docs'),
211
+ });
212
+ expect(nestedState.panel?.items).toEqual(expect.arrayContaining([
213
+ expect.objectContaining({
214
+ title: 'docs',
215
+ subtitle: 'Current folder',
216
+ tokenKind: 'workspace_directory',
217
+ tokenKey: 'docs',
218
+ }),
219
+ expect.objectContaining({
220
+ title: 'Up one folder',
221
+ selectionBehavior: 'navigate',
222
+ }),
223
+ ]));
224
+ });
225
+ });
@@ -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
+ referencePath: null,
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
+ referencePath: null,
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
+ referencePath: null,
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
+ referencePath: null,
248
+ serverPathEntries: [],
249
+ serverPathSearchError: null,
227
250
  skillRecords: [],
228
251
  },
229
252
  plugins: [createPlugin({ onSelectPanelApp })],
@@ -1,10 +1,15 @@
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
3
 
4
4
  export type ChatInputProductPluginData = {
5
5
  isPanelAppsLoading: boolean;
6
+ isServerPathSearchLoading: boolean;
6
7
  isSkillsLoading: boolean;
7
8
  panelApps: readonly PanelAppEntryView[];
9
+ projectRoot: string;
8
10
  recentSkillValues: readonly string[];
11
+ referencePath: string | null;
12
+ serverPathEntries: readonly ServerPathSearchEntryView[];
13
+ serverPathSearchError: string | null;
9
14
  skillRecords: readonly ChatSkillRecord[];
10
15
  };
@@ -0,0 +1,357 @@
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 { ChatInputProductPluginData } from './chat-input-product-plugin-adapters.types';
14
+ import {
15
+ scoreInputSurfaceSearchCandidate,
16
+ resolveInputSurfaceMatchTier,
17
+ } from './input-surface-search.utils';
18
+
19
+ const FILES_NAVIGATION_ITEM_KEY = 'context-reference:navigate:files';
20
+ const ROOT_NAVIGATION_ITEM_KEY = 'context-reference:navigate:root';
21
+ const WORKSPACE_SECTION_KEY = 'workspace';
22
+ const PANEL_APP_SECTION_KEY = 'panel-apps';
23
+
24
+ export const CONTEXT_REFERENCE_TRIGGER_SPEC: ChatInputSurfaceTriggerSpec = {
25
+ key: 'context-reference',
26
+ marker: '@',
27
+ };
28
+
29
+ export type PanelAppInputSurfaceItemTexts = {
30
+ appIdLabel: string;
31
+ fileLabel: string;
32
+ noDescriptionLabel: string;
33
+ subtitle: string;
34
+ };
35
+
36
+ export type ContextReferenceInputSurfaceTexts = {
37
+ backLabel: string;
38
+ backDescription: string;
39
+ backHintLabel: string;
40
+ currentDirectoryLabel: string;
41
+ directoryDescription: string;
42
+ fileDescription: string;
43
+ filesDescription: string;
44
+ filesHintLabel: string;
45
+ filesLabel: string;
46
+ filesSubtitle: string;
47
+ panelAppSectionLabel: string;
48
+ parentLabel: string;
49
+ parentDescription: string;
50
+ parentHintLabel: string;
51
+ projectRootLabel: string;
52
+ searchFailedLabel: string;
53
+ workspaceSectionLabel: string;
54
+ };
55
+
56
+ function getPanelAppActivityTime(entry: PanelAppEntryView): number {
57
+ return Date.parse(entry.lastOpenedAt ?? entry.updatedAt) || 0;
58
+ }
59
+
60
+ function resolveProjectLabel(projectRoot: string): string {
61
+ return projectRoot.split(/[\\/]+/).filter(Boolean).at(-1) ?? projectRoot;
62
+ }
63
+
64
+ function resolvePanelAppInputSurfaceEntries(params: {
65
+ entries: readonly PanelAppEntryView[];
66
+ query: string;
67
+ }): PanelAppEntryView[] {
68
+ const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
69
+ return params.entries
70
+ .map((entry, order) => ({
71
+ entry,
72
+ order,
73
+ score: scoreInputSurfaceSearchCandidate(
74
+ {
75
+ id: entry.appId,
76
+ label: entry.title || entry.appId,
77
+ description: entry.description,
78
+ aliases: [entry.id, entry.fileName],
79
+ },
80
+ params.query,
81
+ ),
82
+ }))
83
+ .filter((entry) => entry.score > 0)
84
+ .sort((left, right) => {
85
+ const leftTier = resolveInputSurfaceMatchTier(left.score);
86
+ const rightTier = resolveInputSurfaceMatchTier(right.score);
87
+ if (rightTier !== leftTier) {
88
+ return rightTier - leftTier;
89
+ }
90
+ if (left.entry.favorite !== right.entry.favorite) {
91
+ return left.entry.favorite ? -1 : 1;
92
+ }
93
+ const rightActivity = getPanelAppActivityTime(right.entry);
94
+ const leftActivity = getPanelAppActivityTime(left.entry);
95
+ if (rightActivity !== leftActivity) {
96
+ return rightActivity - leftActivity;
97
+ }
98
+ if (right.score !== left.score) {
99
+ return right.score - left.score;
100
+ }
101
+ const labelCompare = collator.compare(left.entry.title || left.entry.appId, right.entry.title || right.entry.appId);
102
+ return labelCompare || left.order - right.order;
103
+ })
104
+ .map(({ entry }) => entry);
105
+ }
106
+
107
+ function buildPanelAppInputSurfaceItemEntries(params: {
108
+ entries: readonly PanelAppEntryView[];
109
+ keyPrefix?: string;
110
+ query: string;
111
+ texts: PanelAppInputSurfaceItemTexts;
112
+ }): Array<{ entry: PanelAppEntryView; item: ChatInputSurfaceItem }> {
113
+ const keyPrefix = params.keyPrefix ?? 'panel-app';
114
+ return resolvePanelAppInputSurfaceEntries({
115
+ entries: params.entries,
116
+ query: params.query,
117
+ }).map((entry) => ({
118
+ entry,
119
+ item: {
120
+ key: `${keyPrefix}:${entry.appId}`,
121
+ icon: 'panel-app',
122
+ title: entry.title || entry.appId,
123
+ subtitle: params.texts.subtitle,
124
+ description: (entry.description ?? '').trim() || params.texts.noDescriptionLabel,
125
+ detailLines: [
126
+ `${params.texts.appIdLabel}: ${entry.appId}`,
127
+ `${params.texts.fileLabel}: ${entry.fileName}`,
128
+ ],
129
+ },
130
+ }));
131
+ }
132
+
133
+ export function buildPanelAppInputSurfaceItems(params: {
134
+ entries: readonly PanelAppEntryView[];
135
+ keyPrefix?: string;
136
+ query: string;
137
+ texts: PanelAppInputSurfaceItemTexts;
138
+ }): ChatInputSurfaceItem[] {
139
+ return buildPanelAppInputSurfaceItemEntries(params).map(({ item }) => item);
140
+ }
141
+
142
+ function buildPanelAppReferenceItems(params: {
143
+ entries: readonly PanelAppEntryView[];
144
+ query: string;
145
+ sectionLabel: string;
146
+ texts: PanelAppInputSurfaceItemTexts;
147
+ }): ChatInputSurfaceItem[] {
148
+ return buildPanelAppInputSurfaceItemEntries(params).map(({ entry, item }) => ({
149
+ ...item,
150
+ sectionKey: PANEL_APP_SECTION_KEY,
151
+ sectionLabel: params.sectionLabel,
152
+ tokenKind: 'panel_app',
153
+ tokenKey: entry.appId,
154
+ value: entry.appId,
155
+ }));
156
+ }
157
+
158
+ function buildPathPreview(params: {
159
+ kind: 'directory' | 'file';
160
+ projectRoot: string;
161
+ relativePath: string;
162
+ }) {
163
+ const { kind, projectRoot, relativePath } = params;
164
+ const pathSegments = relativePath.split('/').filter(Boolean);
165
+ return {
166
+ rootLabel: resolveProjectLabel(projectRoot),
167
+ segments: pathSegments.map((label, index) => ({
168
+ label,
169
+ kind: index === pathSegments.length - 1 ? kind : 'directory' as const,
170
+ })),
171
+ };
172
+ }
173
+
174
+ function buildWorkspaceReferenceItems(params: {
175
+ entries: readonly ServerPathSearchEntryView[];
176
+ navigateDirectories: boolean;
177
+ projectRoot: string;
178
+ texts: ContextReferenceInputSurfaceTexts;
179
+ }): ChatInputSurfaceItem[] {
180
+ const projectLabel = resolveProjectLabel(params.projectRoot);
181
+ return params.entries.map((entry) => {
182
+ const navigates = params.navigateDirectories && entry.kind === 'directory';
183
+ return {
184
+ key: `workspace:${entry.kind}:${entry.relativePath}`,
185
+ icon: entry.kind === 'directory' ? 'folder' : 'file',
186
+ title: entry.name,
187
+ subtitle: entry.parentRelativePath || projectLabel,
188
+ description: entry.kind === 'directory'
189
+ ? params.texts.directoryDescription
190
+ : params.texts.fileDescription,
191
+ detailLines: [
192
+ `${params.texts.projectRootLabel}: ${params.projectRoot}`,
193
+ entry.relativePath,
194
+ ],
195
+ sectionKey: WORKSPACE_SECTION_KEY,
196
+ sectionLabel: params.texts.workspaceSectionLabel,
197
+ value: entry.relativePath,
198
+ tokenKind: navigates
199
+ ? undefined
200
+ : entry.kind === 'directory'
201
+ ? CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND
202
+ : CHAT_WORKSPACE_FILE_TOKEN_KIND,
203
+ tokenKey: navigates ? undefined : entry.relativePath,
204
+ selectionBehavior: navigates ? 'navigate' : undefined,
205
+ pathPreview: buildPathPreview({
206
+ kind: entry.kind,
207
+ projectRoot: params.projectRoot,
208
+ relativePath: entry.relativePath,
209
+ }),
210
+ } satisfies ChatInputSurfaceItem;
211
+ });
212
+ }
213
+
214
+ function resolveParentReferencePath(referencePath: string): string | null {
215
+ const segments = referencePath.split('/').filter(Boolean);
216
+ if (segments.length === 0) {
217
+ return null;
218
+ }
219
+ return segments.slice(0, -1).join('/');
220
+ }
221
+
222
+ function buildCurrentDirectoryReferenceItem(params: {
223
+ projectRoot: string;
224
+ referencePath: string;
225
+ texts: ContextReferenceInputSurfaceTexts;
226
+ }): ChatInputSurfaceItem {
227
+ const { projectRoot, referencePath, texts } = params;
228
+ const title = referencePath.split('/').filter(Boolean).at(-1) ?? referencePath;
229
+ return {
230
+ key: `context-reference:current-directory:${referencePath}`,
231
+ icon: 'folder',
232
+ title,
233
+ subtitle: texts.currentDirectoryLabel,
234
+ description: texts.directoryDescription,
235
+ detailLines: [
236
+ `${texts.projectRootLabel}: ${projectRoot}`,
237
+ referencePath,
238
+ ],
239
+ sectionKey: WORKSPACE_SECTION_KEY,
240
+ sectionLabel: texts.workspaceSectionLabel,
241
+ value: referencePath,
242
+ tokenKind: CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
243
+ tokenKey: referencePath,
244
+ pathPreview: buildPathPreview({
245
+ kind: 'directory',
246
+ projectRoot,
247
+ relativePath: referencePath,
248
+ }),
249
+ };
250
+ }
251
+
252
+ function buildFilesNavigationItem(texts: ContextReferenceInputSurfaceTexts): ChatInputSurfaceItem {
253
+ return {
254
+ key: FILES_NAVIGATION_ITEM_KEY,
255
+ icon: 'files',
256
+ title: texts.filesLabel,
257
+ subtitle: texts.filesSubtitle,
258
+ description: texts.filesDescription,
259
+ detailLines: [],
260
+ hintLabel: texts.filesHintLabel,
261
+ selectionBehavior: 'navigate',
262
+ };
263
+ }
264
+
265
+ function buildBackNavigationItem(params: {
266
+ referencePath: string;
267
+ texts: ContextReferenceInputSurfaceTexts;
268
+ }): ChatInputSurfaceItem {
269
+ const { referencePath, texts } = params;
270
+ const isProjectRoot = !referencePath;
271
+ return {
272
+ key: ROOT_NAVIGATION_ITEM_KEY,
273
+ icon: 'back',
274
+ title: isProjectRoot ? texts.backLabel : texts.parentLabel,
275
+ subtitle: '',
276
+ description: isProjectRoot ? texts.backDescription : texts.parentDescription,
277
+ detailLines: [],
278
+ hintLabel: isProjectRoot ? texts.backHintLabel : texts.parentHintLabel,
279
+ value: resolveParentReferencePath(referencePath) ?? '',
280
+ selectionBehavior: 'navigate',
281
+ };
282
+ }
283
+
284
+ export function createContextReferenceInputSurfacePlugin(params: {
285
+ itemTexts: {
286
+ context: ContextReferenceInputSurfaceTexts;
287
+ panelApp: PanelAppInputSurfaceItemTexts;
288
+ };
289
+ menuTexts: ChatInputSurfaceMenuTexts;
290
+ onNavigate: (path: string | null) => void;
291
+ }): ChatInputSurfacePlugin<ChatInputProductPluginData> {
292
+ return createInputSurfaceTriggeredPanelPlugin({
293
+ key: 'context-reference',
294
+ trigger: CONTEXT_REFERENCE_TRIGGER_SPEC,
295
+ resolvePanel: ({ data, trigger }) => {
296
+ const isFilesMode = data.referencePath !== null;
297
+ const isBrowsing = isFilesMode && !trigger.query.trim();
298
+ const workspaceItems = buildWorkspaceReferenceItems({
299
+ entries: data.serverPathEntries,
300
+ navigateDirectories: isBrowsing,
301
+ projectRoot: data.projectRoot,
302
+ texts: params.itemTexts.context,
303
+ });
304
+ const currentDirectoryItem = isBrowsing && data.referencePath
305
+ ? buildCurrentDirectoryReferenceItem({
306
+ projectRoot: data.projectRoot,
307
+ referencePath: data.referencePath,
308
+ texts: params.itemTexts.context,
309
+ })
310
+ : null;
311
+ const items = isFilesMode
312
+ ? [
313
+ buildBackNavigationItem({
314
+ referencePath: data.referencePath ?? '',
315
+ texts: params.itemTexts.context,
316
+ }),
317
+ ...(currentDirectoryItem ? [currentDirectoryItem] : []),
318
+ ...workspaceItems,
319
+ ]
320
+ : [
321
+ buildFilesNavigationItem(params.itemTexts.context),
322
+ ...(trigger.query ? workspaceItems : []),
323
+ ...buildPanelAppReferenceItems({
324
+ entries: data.panelApps,
325
+ query: trigger.query,
326
+ sectionLabel: params.itemTexts.context.panelAppSectionLabel,
327
+ texts: params.itemTexts.panelApp,
328
+ }),
329
+ ];
330
+ const relevantLoading = isFilesMode
331
+ ? data.isServerPathSearchLoading
332
+ : data.isPanelAppsLoading || (Boolean(trigger.query) && data.isServerPathSearchLoading);
333
+ return {
334
+ isLoading: isFilesMode
335
+ ? relevantLoading
336
+ : relevantLoading && items.length === 0,
337
+ items,
338
+ notice: data.serverPathSearchError
339
+ ? {
340
+ message: `${params.itemTexts.context.searchFailedLabel}: ${data.serverPathSearchError}`,
341
+ tone: 'error',
342
+ }
343
+ : undefined,
344
+ onSelectItem: (item) => {
345
+ if (item.key === FILES_NAVIGATION_ITEM_KEY) {
346
+ params.onNavigate('');
347
+ } else if (item.key === ROOT_NAVIGATION_ITEM_KEY) {
348
+ params.onNavigate(data.referencePath ? item.value ?? '' : null);
349
+ } else if (item.selectionBehavior === 'navigate' && item.value) {
350
+ params.onNavigate(item.value);
351
+ }
352
+ },
353
+ texts: params.menuTexts,
354
+ };
355
+ },
356
+ });
357
+ }