@nextclaw/ui 0.15.18 → 0.15.20

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 (114) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/assets/api-tWUa6l9J.js +13 -0
  3. package/dist/assets/app-presenter-provider-s-FrOinZ.js +106 -0
  4. package/dist/assets/{appearance-settings-page-BkQaPtQ-.js → appearance-settings-page-dMRBD7ni.js} +1 -1
  5. package/dist/assets/{book-open-WsEnuW9P.js → book-open-BQ29ZmDE.js} +1 -1
  6. package/dist/assets/{card-xVrS1b1T.js → card-CtKueklV.js} +1 -1
  7. package/dist/assets/{channels-list-page-CAWuTHSM.js → channels-list-page-C8y3rqYt.js} +2 -2
  8. package/dist/assets/chat-page-MXBltS3G.js +1 -0
  9. package/dist/assets/{config-split-page-xj0UtMow.js → config-split-page-uC6d_r9c.js} +1 -1
  10. package/dist/assets/{confirm-dialog-e2YCUzt1.js → confirm-dialog-C1pbqHZX.js} +1 -1
  11. package/dist/assets/desktop-update-config-DssP8LCk.js +1 -0
  12. package/dist/assets/{dist-Ca-jiFrK.js → dist-rYHnBo8t.js} +1 -1
  13. package/dist/assets/doc-browser-AXF1zRsM.js +1 -0
  14. package/dist/assets/doc-browser-D8ng4EUc.js +1 -0
  15. package/dist/assets/doc-browser-context-CPHNzQ8m.js +1 -0
  16. package/dist/assets/{eye-B30J471A.js → eye-CiJz3ODY.js} +1 -1
  17. package/dist/assets/{form-actions-CnSzu3WD.js → form-actions-BnZVzLZf.js} +1 -1
  18. package/dist/assets/index-CocHWEsI.js +2 -0
  19. package/dist/assets/index-DN6KM7WD.css +1 -0
  20. package/dist/assets/loader-circle-AOpK5kkW.js +1 -0
  21. package/dist/assets/{logo-badge-CISsltAo.js → logo-badge-BXI-Kdhd.js} +1 -1
  22. package/dist/assets/mcp-marketplace-page-5ogeBHnl.js +1 -0
  23. package/dist/assets/mcp-marketplace-page-CxNe-Q9Q.js +9 -0
  24. package/dist/assets/model-config-page-CR_mgEVp.js +1 -0
  25. package/dist/assets/{navigation-link-qPPUiLt0.js → navigation-link-C1cvB7y4.js} +1 -1
  26. package/dist/assets/plus-DI37QuHc.js +1 -0
  27. package/dist/assets/{popover-DHx55dbc.js → popover-ZoDwVwwd.js} +1 -1
  28. package/dist/assets/{provider-scoped-model-input-1TJxAhx6.js → provider-scoped-model-input-DIL6Ex_d.js} +1 -1
  29. package/dist/assets/{providers-config-page-BwHtHIPe.js → providers-config-page-D2NXVctW.js} +1 -1
  30. package/dist/assets/react-39CDNZlM.js +8 -0
  31. package/dist/assets/{refresh-cw-C7bynrWX.js → refresh-cw-DKewcdAl.js} +1 -1
  32. package/dist/assets/remote-DYOOO1YG.js +1 -0
  33. package/dist/assets/rotate-cw-PmqA8LQT.js +1 -0
  34. package/dist/assets/{runtime-config-page-BLrTVO3n.js → runtime-config-page-0lb5NyKc.js} +1 -1
  35. package/dist/assets/{save-CsRsFtHS.js → save-BrckuAXY.js} +1 -1
  36. package/dist/assets/{search-D0HtJIL-.js → search-BhXQB2TO.js} +1 -1
  37. package/dist/assets/{search-config-page-n1GElgij.js → search-config-page-DFOQ6SDJ.js} +1 -1
  38. package/dist/assets/{secrets-config-page-DbUc3Ggi.js → secrets-config-page-BqqeClqE.js} +2 -2
  39. package/dist/assets/security-config-13AtPIlm.js +1 -0
  40. package/dist/assets/{select-C1zfp9zM.js → select-CVzGGT38.js} +1 -1
  41. package/dist/assets/{settings-2-Ba3cLSN2.js → settings-2-BP6L2enG.js} +1 -1
  42. package/dist/assets/skeleton-OwCNQJ7M.js +1 -0
  43. package/dist/assets/{tabs-DmKgMKzb.js → tabs-B-QWjKCL.js} +1 -1
  44. package/dist/assets/{tag-chip-2vwlVUwM.js → tag-chip-C88mxjwx.js} +1 -1
  45. package/dist/assets/{trash-2-CUpWjNHu.js → trash-2-ck3t33I3.js} +1 -1
  46. package/dist/assets/use-config-BAr5bH2c.js +1 -0
  47. package/dist/assets/{use-confirm-dialog-CeUEE4p5.js → use-confirm-dialog-C4e0nG_4.js} +1 -1
  48. package/dist/assets/{use-viewport-layout-DaKJ-kap.js → use-viewport-layout-k1isXfNm.js} +1 -1
  49. package/dist/assets/x-C1ijNWcE.js +1 -0
  50. package/dist/index.html +28 -28
  51. package/package.json +9 -9
  52. package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +28 -0
  53. package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +4 -5
  54. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +13 -0
  55. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +31 -8
  56. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +79 -9
  57. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +16 -4
  58. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -2
  59. package/src/features/chat/features/input/input-surface-plugins/context-reference-items.utils.ts +240 -0
  60. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +62 -173
  61. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +29 -0
  62. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
  63. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -5
  64. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +35 -0
  65. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +12 -1
  66. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +3 -1
  67. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +13 -5
  68. package/src/features/chat/features/message/components/chat-message-list.container.tsx +10 -5
  69. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +9 -0
  70. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +4 -1
  71. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +29 -0
  72. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  73. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +32 -14
  74. package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +41 -0
  75. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +4 -0
  76. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +15 -2
  77. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +11 -0
  78. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +1 -0
  79. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +11 -8
  80. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +6 -0
  81. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  82. package/src/features/chat/managers/chat-ui.manager.ts +10 -4
  83. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +26 -0
  84. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  85. package/src/features/chat/stores/chat-thread.store.ts +6 -1
  86. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +32 -0
  87. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +2 -0
  88. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +20 -38
  89. package/src/shared/components/doc-browser/types/doc-browser.types.ts +5 -0
  90. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +63 -0
  91. package/src/shared/hooks/use-projects.ts +2 -1
  92. package/src/shared/lib/i18n/locales/en-US/chat.json +14 -4
  93. package/src/shared/lib/i18n/locales/zh-CN/chat.json +14 -4
  94. package/dist/assets/api-D5UCLVE_.js +0 -13
  95. package/dist/assets/app-presenter-provider-De7Zwitu.js +0 -106
  96. package/dist/assets/chat-page-AvOuaxuv.js +0 -1
  97. package/dist/assets/desktop-update-config-D5NIaimo.js +0 -1
  98. package/dist/assets/doc-browser-B2tNEKrJ.js +0 -1
  99. package/dist/assets/doc-browser-CZX6I6CU.js +0 -1
  100. package/dist/assets/doc-browser-context-CGz0LULR.js +0 -1
  101. package/dist/assets/index-B5AoUrMf.js +0 -2
  102. package/dist/assets/index-QveCjxCj.css +0 -1
  103. package/dist/assets/loader-circle-DjwSRQHW.js +0 -1
  104. package/dist/assets/mcp-marketplace-page-BZa7p7Qf.js +0 -1
  105. package/dist/assets/mcp-marketplace-page-CPcPzMUQ.js +0 -9
  106. package/dist/assets/model-config-page-D6ex3_NM.js +0 -1
  107. package/dist/assets/plus-C45awBb1.js +0 -1
  108. package/dist/assets/react-DerwD2EA.js +0 -8
  109. package/dist/assets/remote-B6TXClIg.js +0 -1
  110. package/dist/assets/rotate-cw-Bhx0XkQP.js +0 -1
  111. package/dist/assets/security-config-n2WHhHXV.js +0 -1
  112. package/dist/assets/skeleton-DL5npsVL.js +0 -1
  113. package/dist/assets/use-config-BvxDWV-M.js +0 -1
  114. package/dist/assets/x-DGdYT7rH.js +0 -1
@@ -9,6 +9,14 @@ const usePanelAppsMock = vi.hoisted(() =>
9
9
  isLoading: false,
10
10
  })),
11
11
  );
12
+ const useProjectsMock = vi.hoisted(() =>
13
+ vi.fn(() => ({
14
+ data: { projects: [] },
15
+ error: null,
16
+ isFetching: false,
17
+ isLoading: false,
18
+ })),
19
+ );
12
20
  const useServerPathSearchMock = vi.hoisted(() =>
13
21
  vi.fn(() => ({
14
22
  data: { entries: [] },
@@ -38,6 +46,9 @@ const useServerPathBrowseMock = vi.hoisted(() =>
38
46
  vi.mock('@/features/panel-apps', () => ({
39
47
  usePanelApps: usePanelAppsMock,
40
48
  }));
49
+ vi.mock('@/shared/hooks/use-projects', () => ({
50
+ useProjects: useProjectsMock,
51
+ }));
41
52
  vi.mock('@/shared/hooks/use-server-path-search', () => ({
42
53
  useServerPathSearch: useServerPathSearchMock,
43
54
  }));
@@ -68,6 +79,7 @@ function createHookParams() {
68
79
 
69
80
  beforeEach(() => {
70
81
  usePanelAppsMock.mockClear();
82
+ useProjectsMock.mockClear();
71
83
  useServerPathBrowseMock.mockClear();
72
84
  useServerPathSearchMock.mockClear();
73
85
  });
@@ -84,6 +96,7 @@ it('browses nested folders while reserving search for project-wide queries', ()
84
96
  start: 0,
85
97
  }),
86
98
  );
99
+ expect(useProjectsMock).toHaveBeenLastCalledWith({ enabled: true });
87
100
  const filesItem = result.current.inputSurfaceState.panel?.items.find(
88
101
  (item) => item.selectionBehavior === 'navigate',
89
102
  );
@@ -10,11 +10,13 @@ import {
10
10
  CONTEXT_REFERENCE_TRIGGER_SPEC,
11
11
  createContextReferenceInputSurfacePlugin,
12
12
  } from '@/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils';
13
+ import type { ContextReferenceLocation } from '@/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types';
13
14
  import {
14
15
  createSlashCommandInputSurfacePlugin,
15
16
  type ChatSlashCommandDescriptor,
16
17
  } from '@/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils';
17
18
  import type { ChatInputBarAdapterTexts, ChatSkillRecord } from '@/features/chat/types/chat-input-bar.types';
19
+ import { useProjects } from '@/shared/hooks/use-projects';
18
20
  import { useServerPathBrowse } from '@/shared/hooks/use-server-path-browse';
19
21
  import { useServerPathSearch } from '@/shared/hooks/use-server-path-search';
20
22
  import type { ServerPathEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
@@ -39,7 +41,7 @@ type UseChatInputSurfaceStateParams = {
39
41
  function buildChatInputSurfacePlugins(params: {
40
42
  commands: readonly ChatSlashCommandDescriptor[];
41
43
  language: I18nLanguage;
42
- onNavigate: (path: string | null) => void;
44
+ onNavigate: (location: ContextReferenceLocation) => void;
43
45
  onSelectPanelApp: (appId: string) => void;
44
46
  onSelectSkill: (skillRef: string) => void;
45
47
  slashTexts: ChatInputSurfaceSlashTexts;
@@ -105,6 +107,15 @@ function buildChatInputSurfacePlugins(params: {
105
107
  parentLabel: t('chatContextReferenceParent', language),
106
108
  parentDescription: t('chatContextReferenceParentDescription', language),
107
109
  parentHintLabel: t('chatContextReferenceParentHint', language),
110
+ projectDescription: t('chatContextReferenceProjectDescription', language),
111
+ projectPathLabel: t('chatContextReferenceProjectPath', language),
112
+ projectSectionLabel: t('chatContextReferenceProjectSection', language),
113
+ projectSubtitle: t('chatContextReferenceProjectType', language),
114
+ projectsDescription: t('chatContextReferenceProjectsDescription', language),
115
+ projectsHintLabel: t('chatContextReferenceProjectsHint', language),
116
+ projectsLabel: t('chatContextReferenceProjectsLabel', language),
117
+ projectsLoadFailedLabel: t('chatContextReferenceProjectsLoadFailed', language),
118
+ projectsSubtitle: t('chatContextReferenceProjectsType', language),
108
119
  projectRootLabel: t('chatContextReferenceProjectRoot', language),
109
120
  searchFailedLabel: t('chatContextReferenceSearchFailed', language),
110
121
  workspaceSectionLabel: t('chatContextReferenceWorkspaceSection', language),
@@ -150,7 +161,9 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
150
161
  skillRecords,
151
162
  commands,
152
163
  } = params;
153
- const [referencePath, setReferencePath] = useState<string | null>(null);
164
+ const [referenceLocation, setReferenceLocation] = useState<ContextReferenceLocation>({
165
+ view: 'root',
166
+ });
154
167
  const [inputSurfaceTrigger, setInputSurfaceTriggerState] = useState<ChatInputSurfaceTrigger | null>(null);
155
168
  const inputSurfaceTriggerSignatureRef = useRef('null');
156
169
  const setInputSurfaceTrigger = useCallback((nextTrigger: ChatInputSurfaceTrigger | null): void => {
@@ -158,23 +171,25 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
158
171
  if (inputSurfaceTriggerSignatureRef.current === nextSignature) return;
159
172
  inputSurfaceTriggerSignatureRef.current = nextSignature;
160
173
  if (!nextTrigger) {
161
- setReferencePath(null);
174
+ setReferenceLocation({ view: 'root' });
162
175
  }
163
176
  setInputSurfaceTriggerState(nextTrigger);
164
177
  }, []);
165
178
  const isContextReferenceTrigger = inputSurfaceTrigger?.key === CONTEXT_REFERENCE_TRIGGER_SPEC.key;
179
+ const referencePath = referenceLocation.view === 'files' ? referenceLocation.path : '';
166
180
  const deferredReferenceQuery = useDeferredValue(
167
181
  isContextReferenceTrigger ? inputSurfaceTrigger.query : '',
168
182
  );
169
183
  const isBrowsingServerPaths = Boolean(
170
184
  isContextReferenceTrigger &&
171
185
  projectRoot &&
172
- referencePath !== null &&
186
+ referenceLocation.view === 'files' &&
173
187
  !deferredReferenceQuery.trim(),
174
188
  );
175
189
  const shouldSearchServerPaths = Boolean(
176
190
  isContextReferenceTrigger &&
177
191
  projectRoot &&
192
+ referenceLocation.view !== 'projects' &&
178
193
  deferredReferenceQuery.trim(),
179
194
  );
180
195
  const serverPathBrowse = useServerPathBrowse({
@@ -193,7 +208,7 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
193
208
  () => isBrowsingServerPaths
194
209
  ? buildBrowsedReferenceEntries({
195
210
  entries: serverPathBrowse.data?.entries ?? [],
196
- parentRelativePath: referencePath ?? '',
211
+ parentRelativePath: referencePath,
197
212
  })
198
213
  : serverPathSearch.data?.entries ?? [],
199
214
  [
@@ -207,12 +222,13 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
207
222
  isContextReferenceTrigger ||
208
223
  inputSurfaceTrigger?.key === CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC.key;
209
224
  const panelApps = usePanelApps({ enabled: shouldLoadPanelApps });
225
+ const projects = useProjects({ enabled: isContextReferenceTrigger });
210
226
 
211
227
  const inputSurfacePlugins = useMemo(
212
228
  () => buildChatInputSurfacePlugins({
213
229
  commands,
214
230
  language,
215
- onNavigate: setReferencePath,
231
+ onNavigate: setReferenceLocation,
216
232
  onSelectPanelApp,
217
233
  onSelectSkill,
218
234
  slashTexts: itemTexts.slashTexts,
@@ -229,12 +245,15 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
229
245
  () => resolveChatInputSurfaceState({
230
246
  data: {
231
247
  isPanelAppsLoading: panelApps.isLoading || panelApps.isFetching,
248
+ isProjectsLoading: projects.isLoading || projects.isFetching,
232
249
  isServerPathSearchLoading: activeServerPathQuery.isLoading || activeServerPathQuery.isFetching,
233
250
  isSkillsLoading,
234
251
  panelApps: panelApps.data?.entries ?? [],
235
252
  projectRoot,
253
+ projects: projects.data?.projects ?? [],
254
+ projectsError: projects.error instanceof Error ? projects.error.message : null,
236
255
  recentSkillValues,
237
- referencePath,
256
+ referenceLocation,
238
257
  serverPathEntries,
239
258
  serverPathSearchError: activeServerPathQuery.error instanceof Error
240
259
  ? activeServerPathQuery.error.message
@@ -251,10 +270,14 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
251
270
  panelApps.isFetching,
252
271
  panelApps.isLoading,
253
272
  projectRoot,
273
+ projects.data?.projects,
274
+ projects.error,
275
+ projects.isFetching,
276
+ projects.isLoading,
254
277
  activeServerPathQuery.error,
255
278
  activeServerPathQuery.isFetching,
256
279
  activeServerPathQuery.isLoading,
257
- referencePath,
280
+ referenceLocation,
258
281
  serverPathEntries,
259
282
  isSkillsLoading,
260
283
  recentSkillValues,
@@ -4,7 +4,8 @@ import {
4
4
  CONTEXT_REFERENCE_TRIGGER_SPEC,
5
5
  createContextReferenceInputSurfacePlugin,
6
6
  } from '@/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils';
7
- import type { PanelAppEntryView } from '@/shared/lib/api';
7
+ import type { ContextReferenceLocation } from '@/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types';
8
+ import type { PanelAppEntryView, ProjectView } from '@/shared/lib/api';
8
9
 
9
10
  function createPanelApp(): PanelAppEntryView {
10
11
  return {
@@ -25,6 +26,15 @@ function createPanelApp(): PanelAppEntryView {
25
26
  };
26
27
  }
27
28
 
29
+ function createProject(): ProjectView {
30
+ return {
31
+ name: 'NextClaw',
32
+ rootPath: '/tmp/nextclaw',
33
+ createdAt: '2026-07-28T00:00:00.000Z',
34
+ updatedAt: '2026-07-28T00:00:00.000Z',
35
+ };
36
+ }
37
+
28
38
  function createPlugin(onNavigate = vi.fn()) {
29
39
  return createContextReferenceInputSurfacePlugin({
30
40
  itemTexts: {
@@ -43,6 +53,15 @@ function createPlugin(onNavigate = vi.fn()) {
43
53
  parentLabel: 'Up one folder',
44
54
  parentDescription: 'Browse parent',
45
55
  parentHintLabel: 'Enter to browse parent',
56
+ projectDescription: 'Project context',
57
+ projectPathLabel: 'Project path',
58
+ projectSectionLabel: 'Projects',
59
+ projectSubtitle: 'Project',
60
+ projectsDescription: 'Browse registered projects',
61
+ projectsHintLabel: 'Enter to browse projects',
62
+ projectsLabel: 'Projects',
63
+ projectsLoadFailedLabel: 'Projects failed',
64
+ projectsSubtitle: 'Project Context',
46
65
  projectRootLabel: 'Project directory',
47
66
  searchFailedLabel: 'Search failed',
48
67
  workspaceSectionLabel: 'Files & Folders',
@@ -65,15 +84,18 @@ function createPlugin(onNavigate = vi.fn()) {
65
84
  });
66
85
  }
67
86
 
68
- function createData(referencePath: string | null = null) {
87
+ function createData(referenceLocation: ContextReferenceLocation = { view: 'root' }) {
69
88
  return {
70
89
  isPanelAppsLoading: false,
90
+ isProjectsLoading: false,
71
91
  isServerPathSearchLoading: false,
72
92
  isSkillsLoading: false,
73
93
  panelApps: [createPanelApp()],
74
94
  projectRoot: '/tmp/project',
95
+ projects: [createProject()],
96
+ projectsError: null,
75
97
  recentSkillValues: [],
76
- referencePath,
98
+ referenceLocation,
77
99
  serverPathEntries: [
78
100
  {
79
101
  name: 'server-path.ts',
@@ -98,7 +120,7 @@ function createData(referencePath: string | null = null) {
98
120
  }
99
121
 
100
122
  describe('context reference input surface plugin', () => {
101
- it('shows Files & Folders and panel apps in the root @ menu', () => {
123
+ it('shows Files & Folders, Projects, and panel apps in the root @ menu', () => {
102
124
  const state = resolveChatInputSurfaceState({
103
125
  plugins: [createPlugin()],
104
126
  trigger: {
@@ -116,6 +138,11 @@ describe('context reference input surface plugin', () => {
116
138
  icon: 'files',
117
139
  selectionBehavior: 'navigate',
118
140
  }),
141
+ expect.objectContaining({
142
+ title: 'Projects',
143
+ icon: 'project',
144
+ selectionBehavior: 'navigate',
145
+ }),
119
146
  expect.objectContaining({
120
147
  key: 'panel-app:task-board',
121
148
  tokenKind: 'panel_app',
@@ -133,7 +160,7 @@ describe('context reference input surface plugin', () => {
133
160
  start: 0,
134
161
  end: 7,
135
162
  },
136
- data: createData(''),
163
+ data: createData({ view: 'files', path: '' }),
137
164
  });
138
165
 
139
166
  expect(state.panel?.items.slice(1)).toEqual([
@@ -173,7 +200,7 @@ describe('context reference input surface plugin', () => {
173
200
 
174
201
  state.panel?.onSelectItem?.(item!);
175
202
 
176
- expect(onNavigate).toHaveBeenCalledWith('');
203
+ expect(onNavigate).toHaveBeenCalledWith({ view: 'files', path: '' });
177
204
  expect(item).not.toHaveProperty('tokenKind');
178
205
  });
179
206
 
@@ -187,7 +214,7 @@ describe('context reference input surface plugin', () => {
187
214
  start: 0,
188
215
  end: 1,
189
216
  },
190
- data: createData(''),
217
+ data: createData({ view: 'files', path: '' }),
191
218
  });
192
219
  const directoryItem = rootState.panel?.items.find((item) => item.title === 'docs');
193
220
 
@@ -197,7 +224,7 @@ describe('context reference input surface plugin', () => {
197
224
  });
198
225
  expect(directoryItem?.tokenKind).toBeUndefined();
199
226
  rootState.panel?.onSelectItem?.(directoryItem!);
200
- expect(onNavigate).toHaveBeenCalledWith('docs');
227
+ expect(onNavigate).toHaveBeenCalledWith({ view: 'files', path: 'docs' });
201
228
 
202
229
  const nestedState = resolveChatInputSurfaceState({
203
230
  plugins: [createPlugin(onNavigate)],
@@ -207,7 +234,7 @@ describe('context reference input surface plugin', () => {
207
234
  start: 0,
208
235
  end: 1,
209
236
  },
210
- data: createData('docs'),
237
+ data: createData({ view: 'files', path: 'docs' }),
211
238
  });
212
239
  expect(nestedState.panel?.items).toEqual(expect.arrayContaining([
213
240
  expect.objectContaining({
@@ -222,4 +249,47 @@ describe('context reference input surface plugin', () => {
222
249
  }),
223
250
  ]));
224
251
  });
252
+
253
+ it('opens the project view and builds semantic project tokens', () => {
254
+ const onNavigate = vi.fn();
255
+ const rootState = resolveChatInputSurfaceState({
256
+ plugins: [createPlugin(onNavigate)],
257
+ trigger: {
258
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
259
+ query: '',
260
+ start: 0,
261
+ end: 1,
262
+ },
263
+ data: createData(),
264
+ });
265
+ const projectsItem = rootState.panel?.items.find((item) => item.title === 'Projects');
266
+
267
+ rootState.panel?.onSelectItem?.(projectsItem!);
268
+ expect(onNavigate).toHaveBeenCalledWith({ view: 'projects' });
269
+
270
+ const projectsState = resolveChatInputSurfaceState({
271
+ plugins: [createPlugin(onNavigate)],
272
+ trigger: {
273
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
274
+ query: 'next',
275
+ start: 0,
276
+ end: 5,
277
+ },
278
+ data: createData({ view: 'projects' }),
279
+ });
280
+
281
+ expect(projectsState.panel?.items).toEqual([
282
+ expect.objectContaining({
283
+ title: 'Back',
284
+ selectionBehavior: 'navigate',
285
+ }),
286
+ expect.objectContaining({
287
+ title: 'NextClaw',
288
+ icon: 'project',
289
+ tokenKind: 'project',
290
+ tokenKey: '/tmp/nextclaw',
291
+ detailLines: ['Project path: /tmp/nextclaw'],
292
+ }),
293
+ ]);
294
+ });
225
295
  });
@@ -86,6 +86,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
86
86
  const state = resolveChatInputSurfaceState({
87
87
  data: {
88
88
  isPanelAppsLoading: false,
89
+ isProjectsLoading: false,
89
90
  isServerPathSearchLoading: false,
90
91
  isSkillsLoading: false,
91
92
  panelApps: [
@@ -96,8 +97,10 @@ describe('createSlashCommandInputSurfacePlugin', () => {
96
97
  }),
97
98
  ],
98
99
  projectRoot: '/tmp/project',
100
+ projects: [],
101
+ projectsError: null,
99
102
  recentSkillValues: [],
100
- referencePath: null,
103
+ referenceLocation: { view: 'root' },
101
104
  serverPathEntries: [],
102
105
  serverPathSearchError: null,
103
106
  skillRecords: [
@@ -153,12 +156,15 @@ describe('createSlashCommandInputSurfacePlugin', () => {
153
156
  const state = resolveChatInputSurfaceState({
154
157
  data: {
155
158
  isPanelAppsLoading: false,
159
+ isProjectsLoading: false,
156
160
  isServerPathSearchLoading: false,
157
161
  isSkillsLoading: false,
158
162
  panelApps: [],
159
163
  projectRoot: '/tmp/project',
164
+ projects: [],
165
+ projectsError: null,
160
166
  recentSkillValues: [],
161
- referencePath: null,
167
+ referenceLocation: { view: 'root' },
162
168
  serverPathEntries: [],
163
169
  serverPathSearchError: null,
164
170
  skillRecords: [
@@ -202,12 +208,15 @@ describe('createSlashCommandInputSurfacePlugin', () => {
202
208
  const state = resolveChatInputSurfaceState({
203
209
  data: {
204
210
  isPanelAppsLoading: false,
211
+ isProjectsLoading: false,
205
212
  isServerPathSearchLoading: false,
206
213
  isSkillsLoading: false,
207
214
  panelApps: [],
208
215
  projectRoot: '/tmp/project',
216
+ projects: [],
217
+ projectsError: null,
209
218
  recentSkillValues: [],
210
- referencePath: null,
219
+ referenceLocation: { view: 'root' },
211
220
  serverPathEntries: [],
212
221
  serverPathSearchError: null,
213
222
  skillRecords: [],
@@ -233,6 +242,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
233
242
  const state = resolveChatInputSurfaceState({
234
243
  data: {
235
244
  isPanelAppsLoading: false,
245
+ isProjectsLoading: false,
236
246
  isServerPathSearchLoading: false,
237
247
  isSkillsLoading: false,
238
248
  panelApps: [
@@ -243,8 +253,10 @@ describe('createSlashCommandInputSurfacePlugin', () => {
243
253
  }),
244
254
  ],
245
255
  projectRoot: '/tmp/project',
256
+ projects: [],
257
+ projectsError: null,
246
258
  recentSkillValues: [],
247
- referencePath: null,
259
+ referenceLocation: { view: 'root' },
248
260
  serverPathEntries: [],
249
261
  serverPathSearchError: null,
250
262
  skillRecords: [],
@@ -1,14 +1,22 @@
1
1
  import type { ChatSkillRecord } from '@/features/chat/types/chat-input-bar.types';
2
- import type { PanelAppEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
2
+ import type { PanelAppEntryView, ProjectView, ServerPathSearchEntryView } from '@/shared/lib/api';
3
+
4
+ export type ContextReferenceLocation =
5
+ | { view: 'root' }
6
+ | { view: 'files'; path: string }
7
+ | { view: 'projects' };
3
8
 
4
9
  export type ChatInputProductPluginData = {
5
10
  isPanelAppsLoading: boolean;
11
+ isProjectsLoading: boolean;
6
12
  isServerPathSearchLoading: boolean;
7
13
  isSkillsLoading: boolean;
8
14
  panelApps: readonly PanelAppEntryView[];
9
15
  projectRoot: string;
16
+ projects: readonly ProjectView[];
17
+ projectsError: string | null;
10
18
  recentSkillValues: readonly string[];
11
- referencePath: string | null;
19
+ referenceLocation: ContextReferenceLocation;
12
20
  serverPathEntries: readonly ServerPathSearchEntryView[];
13
21
  serverPathSearchError: string | null;
14
22
  skillRecords: readonly ChatSkillRecord[];
@@ -0,0 +1,240 @@
1
+ import type { ChatInputSurfaceItem } from '@nextclaw/agent-chat-ui';
2
+ import {
3
+ CHAT_PROJECT_TOKEN_KIND,
4
+ CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
5
+ CHAT_WORKSPACE_FILE_TOKEN_KIND,
6
+ } from '@nextclaw/shared';
7
+ import type { ProjectView, ServerPathSearchEntryView } from '@/shared/lib/api';
8
+ import type { ContextReferenceLocation } from './chat-input-product-plugin-adapters.types';
9
+ import {
10
+ resolveInputSurfaceMatchTier,
11
+ scoreInputSurfaceSearchCandidate,
12
+ } from './input-surface-search.utils';
13
+
14
+ export const FILES_NAVIGATION_ITEM_KEY = 'context-reference:navigate:files';
15
+ export const PROJECTS_NAVIGATION_ITEM_KEY = 'context-reference:navigate:projects';
16
+ export const ROOT_NAVIGATION_ITEM_KEY = 'context-reference:navigate:root';
17
+
18
+ const WORKSPACE_SECTION_KEY = 'workspace';
19
+ const PROJECT_SECTION_KEY = 'projects';
20
+
21
+ export type ContextReferenceInputSurfaceTexts = {
22
+ backLabel: string;
23
+ backDescription: string;
24
+ backHintLabel: string;
25
+ currentDirectoryLabel: string;
26
+ directoryDescription: string;
27
+ fileDescription: string;
28
+ filesDescription: string;
29
+ filesHintLabel: string;
30
+ filesLabel: string;
31
+ filesSubtitle: string;
32
+ panelAppSectionLabel: string;
33
+ parentLabel: string;
34
+ parentDescription: string;
35
+ parentHintLabel: string;
36
+ projectDescription: string;
37
+ projectPathLabel: string;
38
+ projectSectionLabel: string;
39
+ projectSubtitle: string;
40
+ projectsDescription: string;
41
+ projectsHintLabel: string;
42
+ projectsLabel: string;
43
+ projectsLoadFailedLabel: string;
44
+ projectsSubtitle: string;
45
+ projectRootLabel: string;
46
+ searchFailedLabel: string;
47
+ workspaceSectionLabel: string;
48
+ };
49
+
50
+ function resolveProjectLabel(projectRoot: string): string {
51
+ return projectRoot.split(/[\\/]+/).filter(Boolean).at(-1) ?? projectRoot;
52
+ }
53
+
54
+ function buildPathPreview(params: {
55
+ kind: 'directory' | 'file';
56
+ projectRoot: string;
57
+ relativePath: string;
58
+ }) {
59
+ const { kind, projectRoot, relativePath } = params;
60
+ const pathSegments = relativePath.split('/').filter(Boolean);
61
+ return {
62
+ rootLabel: resolveProjectLabel(projectRoot),
63
+ segments: pathSegments.map((label, index) => ({
64
+ label,
65
+ kind: index === pathSegments.length - 1 ? kind : 'directory' as const,
66
+ })),
67
+ };
68
+ }
69
+
70
+ export function buildProjectReferenceItems(params: {
71
+ projects: readonly ProjectView[];
72
+ query: string;
73
+ texts: ContextReferenceInputSurfaceTexts;
74
+ }): ChatInputSurfaceItem[] {
75
+ const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
76
+ return params.projects
77
+ .map((project, order) => ({
78
+ order,
79
+ project,
80
+ score: scoreInputSurfaceSearchCandidate(
81
+ {
82
+ id: project.rootPath,
83
+ label: project.name,
84
+ description: project.rootPath,
85
+ aliases: project.template ? [project.template] : [],
86
+ },
87
+ params.query,
88
+ ),
89
+ }))
90
+ .filter(({ score }) => score > 0)
91
+ .sort((left, right) => {
92
+ const leftTier = resolveInputSurfaceMatchTier(left.score);
93
+ const rightTier = resolveInputSurfaceMatchTier(right.score);
94
+ if (rightTier !== leftTier) {
95
+ return rightTier - leftTier;
96
+ }
97
+ if (right.score !== left.score) {
98
+ return right.score - left.score;
99
+ }
100
+ return collator.compare(left.project.name, right.project.name) || left.order - right.order;
101
+ })
102
+ .map(({ project }) => ({
103
+ key: `context-reference:project:${project.rootPath}`,
104
+ icon: 'project',
105
+ title: project.name,
106
+ subtitle: params.texts.projectSubtitle,
107
+ description: params.texts.projectDescription,
108
+ detailLines: [`${params.texts.projectPathLabel}: ${project.rootPath}`],
109
+ sectionKey: PROJECT_SECTION_KEY,
110
+ sectionLabel: params.texts.projectSectionLabel,
111
+ value: project.rootPath,
112
+ tokenKind: CHAT_PROJECT_TOKEN_KIND,
113
+ tokenKey: project.rootPath,
114
+ }));
115
+ }
116
+
117
+ export function buildWorkspaceReferenceItems(params: {
118
+ entries: readonly ServerPathSearchEntryView[];
119
+ navigateDirectories: boolean;
120
+ projectRoot: string;
121
+ texts: ContextReferenceInputSurfaceTexts;
122
+ }): ChatInputSurfaceItem[] {
123
+ const projectLabel = resolveProjectLabel(params.projectRoot);
124
+ return params.entries.map((entry) => {
125
+ const navigates = params.navigateDirectories && entry.kind === 'directory';
126
+ return {
127
+ key: `workspace:${entry.kind}:${entry.relativePath}`,
128
+ icon: entry.kind === 'directory' ? 'folder' : 'file',
129
+ title: entry.name,
130
+ subtitle: entry.parentRelativePath || projectLabel,
131
+ description: entry.kind === 'directory'
132
+ ? params.texts.directoryDescription
133
+ : params.texts.fileDescription,
134
+ detailLines: [
135
+ `${params.texts.projectRootLabel}: ${params.projectRoot}`,
136
+ entry.relativePath,
137
+ ],
138
+ sectionKey: WORKSPACE_SECTION_KEY,
139
+ sectionLabel: params.texts.workspaceSectionLabel,
140
+ value: entry.relativePath,
141
+ tokenKind: navigates
142
+ ? undefined
143
+ : entry.kind === 'directory'
144
+ ? CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND
145
+ : CHAT_WORKSPACE_FILE_TOKEN_KIND,
146
+ tokenKey: navigates ? undefined : entry.relativePath,
147
+ selectionBehavior: navigates ? 'navigate' : undefined,
148
+ pathPreview: buildPathPreview({
149
+ kind: entry.kind,
150
+ projectRoot: params.projectRoot,
151
+ relativePath: entry.relativePath,
152
+ }),
153
+ } satisfies ChatInputSurfaceItem;
154
+ });
155
+ }
156
+
157
+ export function buildCurrentDirectoryReferenceItem(params: {
158
+ projectRoot: string;
159
+ referencePath: string;
160
+ texts: ContextReferenceInputSurfaceTexts;
161
+ }): ChatInputSurfaceItem {
162
+ const { projectRoot, referencePath, texts } = params;
163
+ const title = referencePath.split('/').filter(Boolean).at(-1) ?? referencePath;
164
+ return {
165
+ key: `context-reference:current-directory:${referencePath}`,
166
+ icon: 'folder',
167
+ title: texts.currentDirectoryLabel,
168
+ subtitle: title,
169
+ description: texts.directoryDescription,
170
+ detailLines: [
171
+ `${texts.projectRootLabel}: ${projectRoot}`,
172
+ referencePath,
173
+ ],
174
+ sectionKey: WORKSPACE_SECTION_KEY,
175
+ sectionLabel: texts.workspaceSectionLabel,
176
+ value: referencePath,
177
+ tokenKind: CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
178
+ tokenKey: referencePath,
179
+ pathPreview: buildPathPreview({
180
+ kind: 'directory',
181
+ projectRoot,
182
+ relativePath: referencePath,
183
+ }),
184
+ };
185
+ }
186
+
187
+ export function buildFilesNavigationItem(
188
+ texts: ContextReferenceInputSurfaceTexts,
189
+ ): ChatInputSurfaceItem {
190
+ return {
191
+ key: FILES_NAVIGATION_ITEM_KEY,
192
+ icon: 'files',
193
+ title: texts.filesLabel,
194
+ subtitle: texts.filesSubtitle,
195
+ description: texts.filesDescription,
196
+ detailLines: [],
197
+ hintLabel: texts.filesHintLabel,
198
+ selectionBehavior: 'navigate',
199
+ };
200
+ }
201
+
202
+ export function buildProjectsNavigationItem(
203
+ texts: ContextReferenceInputSurfaceTexts,
204
+ ): ChatInputSurfaceItem {
205
+ return {
206
+ key: PROJECTS_NAVIGATION_ITEM_KEY,
207
+ icon: 'project',
208
+ title: texts.projectsLabel,
209
+ subtitle: texts.projectsSubtitle,
210
+ description: texts.projectsDescription,
211
+ detailLines: [],
212
+ hintLabel: texts.projectsHintLabel,
213
+ selectionBehavior: 'navigate',
214
+ };
215
+ }
216
+
217
+ function resolveParentReferencePath(referencePath: string): string | null {
218
+ const segments = referencePath.split('/').filter(Boolean);
219
+ return segments.length === 0 ? null : segments.slice(0, -1).join('/');
220
+ }
221
+
222
+ export function buildBackNavigationItem(params: {
223
+ location: ContextReferenceLocation;
224
+ texts: ContextReferenceInputSurfaceTexts;
225
+ }): ChatInputSurfaceItem {
226
+ const { location, texts } = params;
227
+ const referencePath = location.view === 'files' ? location.path : '';
228
+ const isRootView = location.view !== 'files' || !referencePath;
229
+ return {
230
+ key: ROOT_NAVIGATION_ITEM_KEY,
231
+ icon: 'back',
232
+ title: isRootView ? texts.backLabel : texts.parentLabel,
233
+ subtitle: '',
234
+ description: isRootView ? texts.backDescription : texts.parentDescription,
235
+ detailLines: [],
236
+ hintLabel: isRootView ? texts.backHintLabel : texts.parentHintLabel,
237
+ value: resolveParentReferencePath(referencePath) ?? '',
238
+ selectionBehavior: 'navigate',
239
+ };
240
+ }