@nextclaw/ui 0.14.1 → 0.14.2

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 (165) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/assets/{api-DPcvCLfa.js → api-a14tR_z1.js} +7 -7
  3. package/dist/assets/channels-list-page-CFGkm4DT.js +8 -0
  4. package/dist/assets/chat-page-CEdavJXc.js +105 -0
  5. package/dist/assets/{config-split-page-BCJhqdK9.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.js → dist-CLGGLm8b.js} +1 -1
  10. package/dist/assets/doc-browser-5nMWYKBD.js +1 -0
  11. package/dist/assets/doc-browser-BJRLhztv.js +1 -0
  12. package/dist/assets/doc-browser-context-D4JwMMxK.js +1 -0
  13. package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.js → external-link-CdJiG13p.js} +1 -1
  15. package/dist/assets/index-BDN1zzMQ.js +105 -0
  16. package/dist/assets/index-g3uSZXoc.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-BU0kIjhR.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-D6hYXROd.js +1 -0
  21. package/dist/assets/plus-DPjb0piO.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DAX2HgmR.js +1 -0
  23. package/dist/assets/providers-config-page-BzAeaACA.js +1 -0
  24. package/dist/assets/{react-sZz5HSCm.js → react-C6cZgP44.js} +1 -1
  25. package/dist/assets/remote-B0G80uPr.js +1 -0
  26. package/dist/assets/rotate-cw-DnEB1Rju.js +1 -0
  27. package/dist/assets/runtime-config-page-C5xKaV3i.js +1 -0
  28. package/dist/assets/{save-MzjoiHri.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-8ffF2y2C.js} +1 -1
  34. package/dist/assets/use-config-DmT0o1gv.js +1 -0
  35. package/dist/assets/x-BiSSpgqB.js +1 -0
  36. package/dist/index.html +15 -14
  37. package/package.json +9 -9
  38. package/src/app/components/app-presenter-provider.tsx +11 -2
  39. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  40. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  41. package/src/app/components/layout/app-layout.tsx +18 -4
  42. package/src/app/components/layout/sidebar-items.tsx +194 -71
  43. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  44. package/src/app/components/layout/sidebar.tsx +340 -83
  45. package/src/app/managers/viewport-layout.manager.ts +10 -0
  46. package/src/app/stores/viewport-layout.store.ts +39 -3
  47. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  48. package/src/features/agents/components/agents-page.tsx +10 -6
  49. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  50. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  51. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  52. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  53. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +14 -15
  54. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +18 -18
  55. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +16 -14
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  59. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +80 -7
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +59 -92
  62. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +289 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +427 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +204 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  67. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +32 -17
  68. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  69. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  70. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  71. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  72. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  74. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  75. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  76. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  78. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  79. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  80. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  81. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  82. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  83. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  84. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  85. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  86. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  87. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  88. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  89. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  90. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  91. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  92. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  93. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  94. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  95. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  96. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  97. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  98. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  99. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  100. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  101. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  102. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  103. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  104. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  107. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  108. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  109. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  110. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  111. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  112. package/src/features/chat/index.ts +0 -1
  113. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  114. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  115. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  116. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  117. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  118. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  119. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  120. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  121. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  122. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  123. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  124. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  125. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  126. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  127. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  128. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  129. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  130. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  131. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  132. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  133. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  134. package/src/shared/lib/ui-document-title/index.ts +50 -21
  135. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  136. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  137. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  138. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  139. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  140. package/dist/assets/index-7o1WdUbU.js +0 -103
  141. package/dist/assets/index-DQWmEWzk.css +0 -1
  142. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  143. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  144. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  145. package/dist/assets/plus-Bnt56oqG.js +0 -1
  146. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  147. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  148. package/dist/assets/remote-B0LMs8u7.js +0 -1
  149. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  150. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  151. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  152. package/dist/assets/x-CW2KO-Hz.js +0 -1
  153. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  154. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  155. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  156. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  157. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  158. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  159. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  160. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  161. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  162. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  163. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  164. package/src/features/chat/stores/chat-input.store.ts +0 -104
  165. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveChatInputSurfaceState } from '@nextclaw/agent-chat-ui';
3
+ import { createPanelAppReferenceInputSurfacePlugin, PANEL_APP_REFERENCE_TRIGGER_SPEC } from '@/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils';
4
+ import type { PanelAppEntryView } from '@/shared/lib/api';
5
+
6
+ function createPanelApp(overrides: Partial<PanelAppEntryView> & Pick<PanelAppEntryView, 'appId' | 'title'>): PanelAppEntryView {
7
+ return {
8
+ id: overrides.id ?? overrides.appId,
9
+ appId: overrides.appId,
10
+ fileName: overrides.fileName ?? `${overrides.appId}.panel.html`,
11
+ kind: overrides.kind ?? 'single-file',
12
+ title: overrides.title,
13
+ description: overrides.description,
14
+ contentPath: overrides.contentPath ?? `/panels/${overrides.appId}.panel.html`,
15
+ createdAt: overrides.createdAt ?? '2026-06-18T00:00:00.000Z',
16
+ updatedAt: overrides.updatedAt ?? '2026-06-18T00:00:00.000Z',
17
+ sizeBytes: overrides.sizeBytes ?? 100,
18
+ favorite: overrides.favorite ?? false,
19
+ clientDeclared: overrides.clientDeclared ?? false,
20
+ clientGranted: overrides.clientGranted ?? false,
21
+ lastOpenedAt: overrides.lastOpenedAt,
22
+ openCount: overrides.openCount ?? 0,
23
+ };
24
+ }
25
+
26
+ describe('panel app reference input surface plugin', () => {
27
+ it('returns panel app reference items for @ trigger queries', () => {
28
+ const plugin = createPanelAppReferenceInputSurfacePlugin({
29
+ itemTexts: {
30
+ appIdLabel: 'App ID',
31
+ fileLabel: 'File',
32
+ noDescriptionLabel: 'No description',
33
+ subtitle: 'Panel App',
34
+ },
35
+ menuTexts: {
36
+ loadingLabel: 'Loading',
37
+ sectionLabel: 'Panel Apps',
38
+ emptyLabel: 'Empty',
39
+ hintLabel: 'Type @',
40
+ itemHintLabel: 'Enter to reference',
41
+ },
42
+ });
43
+
44
+ const state = resolveChatInputSurfaceState({
45
+ plugins: [plugin],
46
+ trigger: {
47
+ ...PANEL_APP_REFERENCE_TRIGGER_SPEC,
48
+ query: 'task',
49
+ start: 0,
50
+ end: 5,
51
+ },
52
+ data: {
53
+ isPanelAppsLoading: false,
54
+ isSkillsLoading: false,
55
+ panelApps: [
56
+ createPanelApp({
57
+ appId: 'task-board',
58
+ title: 'Task Board',
59
+ description: 'Track tasks',
60
+ }),
61
+ ],
62
+ recentSkillValues: [],
63
+ skillRecords: [],
64
+ },
65
+ });
66
+
67
+ expect(state.panel?.items).toEqual([
68
+ expect.objectContaining({
69
+ key: 'panel-app:task-board',
70
+ title: 'Task Board',
71
+ tokenKind: 'panel_app',
72
+ tokenKey: 'task-board',
73
+ value: 'task-board',
74
+ }),
75
+ ]);
76
+ });
77
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { resolveChatInputSurfaceState } from '@nextclaw/agent-chat-ui';
3
+ import { createSlashCommandInputSurfacePlugin } from '@/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils';
4
+
5
+ function createPlugin(onSelectCommand = vi.fn()) {
6
+ return createSlashCommandInputSurfacePlugin({
7
+ commands: [
8
+ {
9
+ key: 'side-chat',
10
+ title: 'Side chat',
11
+ description: 'Open side chat',
12
+ detailLines: ['Creates a child session on first send'],
13
+ keywords: ['side', 'chat'],
14
+ onSelect: onSelectCommand,
15
+ },
16
+ ],
17
+ itemTexts: {
18
+ noSkillDescription: 'No description',
19
+ slashSkillScopeLabel: 'Scope',
20
+ slashSkillSpecLabel: 'Spec',
21
+ slashSkillSubtitle: 'Skill',
22
+ },
23
+ labels: {
24
+ commandHintLabel: 'Run command',
25
+ commandSectionLabel: 'Commands',
26
+ commandSubtitle: 'Command',
27
+ skillHintLabel: 'Add skill',
28
+ skillSectionLabel: 'Skills',
29
+ },
30
+ menuTexts: {
31
+ emptyLabel: 'No result',
32
+ hintLabel: 'Type /',
33
+ itemHintLabel: 'Select item',
34
+ loadingLabel: 'Loading',
35
+ sectionLabel: 'Slash',
36
+ },
37
+ onSelectSkill: vi.fn(),
38
+ });
39
+ }
40
+
41
+ describe('createSlashCommandInputSurfacePlugin', () => {
42
+ it('places command items before skill items in the slash panel', () => {
43
+ const state = resolveChatInputSurfaceState({
44
+ data: {
45
+ isPanelAppsLoading: false,
46
+ isSkillsLoading: false,
47
+ panelApps: [],
48
+ recentSkillValues: [],
49
+ skillRecords: [
50
+ {
51
+ key: 'web-search',
52
+ label: 'Web Search',
53
+ description: 'Search web',
54
+ },
55
+ ],
56
+ },
57
+ plugins: [createPlugin()],
58
+ trigger: {
59
+ key: 'slash',
60
+ marker: '/',
61
+ query: '',
62
+ start: 0,
63
+ end: 1,
64
+ },
65
+ });
66
+
67
+ expect(state.panel?.items.map((item) => item.key)).toEqual([
68
+ 'command:side-chat',
69
+ 'skill:web-search',
70
+ ]);
71
+ expect(state.panel?.items[0]).toMatchObject({
72
+ sectionKey: 'commands',
73
+ sectionLabel: 'Commands',
74
+ hintLabel: 'Run command',
75
+ });
76
+ expect(state.panel?.items[1]).toMatchObject({
77
+ sectionKey: 'skills',
78
+ sectionLabel: 'Skills',
79
+ hintLabel: 'Add skill',
80
+ });
81
+ });
82
+
83
+ it('runs a command item through the panel selection handler', () => {
84
+ const onSelectCommand = vi.fn();
85
+ const state = resolveChatInputSurfaceState({
86
+ data: {
87
+ isPanelAppsLoading: false,
88
+ isSkillsLoading: false,
89
+ panelApps: [],
90
+ recentSkillValues: [],
91
+ skillRecords: [],
92
+ },
93
+ plugins: [createPlugin(onSelectCommand)],
94
+ trigger: {
95
+ key: 'slash',
96
+ marker: '/',
97
+ query: 'side',
98
+ start: 0,
99
+ end: 5,
100
+ },
101
+ });
102
+
103
+ const item = state.panel?.items[0];
104
+ expect(item?.key).toBe('command:side-chat');
105
+ state.panel?.onSelectItem?.(item!);
106
+ expect(onSelectCommand).toHaveBeenCalledTimes(1);
107
+ });
108
+ });
@@ -0,0 +1,10 @@
1
+ import type { ChatSkillRecord } from '@/features/chat/types/chat-input-bar.types';
2
+ import type { PanelAppEntryView } from '@/shared/lib/api';
3
+
4
+ export type ChatInputProductPluginData = {
5
+ isPanelAppsLoading: boolean;
6
+ isSkillsLoading: boolean;
7
+ panelApps: readonly PanelAppEntryView[];
8
+ recentSkillValues: readonly string[];
9
+ skillRecords: readonly ChatSkillRecord[];
10
+ };
@@ -0,0 +1,108 @@
1
+ const INPUT_SURFACE_MATCH_SCORE = {
2
+ exactId: 1200,
3
+ exactLabel: 1150,
4
+ prefixId: 1000,
5
+ prefixLabel: 950,
6
+ prefixToken: 900,
7
+ containsId: 800,
8
+ containsLabel: 760,
9
+ containsDescription: 500,
10
+ subsequence: 300,
11
+ fallback: 1,
12
+ } as const;
13
+
14
+ export type InputSurfaceSearchCandidate = {
15
+ id: string;
16
+ label: string;
17
+ description?: string;
18
+ aliases?: readonly string[];
19
+ };
20
+
21
+ export function normalizeInputSurfaceSearchText(value: string | null | undefined): string {
22
+ return (value ?? '').trim().toLowerCase();
23
+ }
24
+
25
+ export function buildInputSurfaceRecentOrderIndex(values: readonly string[]): Map<string, number> {
26
+ return new Map(values.map((value, index) => [value, index] as const));
27
+ }
28
+
29
+ function isSubsequenceMatch(query: string, target: string): boolean {
30
+ if (!query || !target) {
31
+ return false;
32
+ }
33
+ let pointer = 0;
34
+ for (const char of target) {
35
+ if (char === query[pointer]) {
36
+ pointer += 1;
37
+ if (pointer >= query.length) {
38
+ return true;
39
+ }
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+
45
+ export function scoreInputSurfaceSearchCandidate(
46
+ candidate: InputSurfaceSearchCandidate,
47
+ query: string,
48
+ ): number {
49
+ const normalizedQuery = normalizeInputSurfaceSearchText(query);
50
+ if (!normalizedQuery) {
51
+ return INPUT_SURFACE_MATCH_SCORE.fallback;
52
+ }
53
+
54
+ const id = normalizeInputSurfaceSearchText(candidate.id);
55
+ const label = normalizeInputSurfaceSearchText(candidate.label || candidate.id);
56
+ const description = normalizeInputSurfaceSearchText(candidate.description);
57
+ const aliases = (candidate.aliases ?? []).map(normalizeInputSurfaceSearchText).filter(Boolean);
58
+ const labelTokens = label.split(/[\s/_-]+/).filter(Boolean);
59
+ const idCandidates = [id, ...aliases];
60
+
61
+ if (idCandidates.some((value) => value === normalizedQuery)) {
62
+ return INPUT_SURFACE_MATCH_SCORE.exactId;
63
+ }
64
+ if (label === normalizedQuery) {
65
+ return INPUT_SURFACE_MATCH_SCORE.exactLabel;
66
+ }
67
+ if (idCandidates.some((value) => value.startsWith(normalizedQuery))) {
68
+ return INPUT_SURFACE_MATCH_SCORE.prefixId;
69
+ }
70
+ if (label.startsWith(normalizedQuery)) {
71
+ return INPUT_SURFACE_MATCH_SCORE.prefixLabel;
72
+ }
73
+ if (labelTokens.some((token) => token.startsWith(normalizedQuery))) {
74
+ return INPUT_SURFACE_MATCH_SCORE.prefixToken;
75
+ }
76
+ if (idCandidates.some((value) => value.includes(normalizedQuery))) {
77
+ return INPUT_SURFACE_MATCH_SCORE.containsId;
78
+ }
79
+ if (label.includes(normalizedQuery)) {
80
+ return INPUT_SURFACE_MATCH_SCORE.containsLabel;
81
+ }
82
+ if (description.includes(normalizedQuery)) {
83
+ return INPUT_SURFACE_MATCH_SCORE.containsDescription;
84
+ }
85
+ if (
86
+ isSubsequenceMatch(normalizedQuery, label) ||
87
+ idCandidates.some((value) => isSubsequenceMatch(normalizedQuery, value))
88
+ ) {
89
+ return INPUT_SURFACE_MATCH_SCORE.subsequence;
90
+ }
91
+ return 0;
92
+ }
93
+
94
+ export function resolveInputSurfaceMatchTier(score: number): number {
95
+ if (score >= INPUT_SURFACE_MATCH_SCORE.exactLabel) {
96
+ return 4;
97
+ }
98
+ if (score >= INPUT_SURFACE_MATCH_SCORE.prefixToken) {
99
+ return 3;
100
+ }
101
+ if (score >= INPUT_SURFACE_MATCH_SCORE.containsLabel) {
102
+ return 2;
103
+ }
104
+ if (score > 0) {
105
+ return 1;
106
+ }
107
+ return 0;
108
+ }
@@ -0,0 +1,108 @@
1
+ import {
2
+ createInputSurfaceReferenceTokenPlugin,
3
+ type ChatInputSurfaceItem,
4
+ type ChatInputSurfaceMenuTexts,
5
+ type ChatInputSurfacePlugin,
6
+ type ChatInputSurfaceTriggerSpec,
7
+ } from '@nextclaw/agent-chat-ui';
8
+ import type { ChatInputProductPluginData } from './chat-input-product-plugin-adapters.types';
9
+ import type { PanelAppEntryView } from '@/shared/lib/api';
10
+ import {
11
+ scoreInputSurfaceSearchCandidate,
12
+ resolveInputSurfaceMatchTier,
13
+ } from './input-surface-search.utils';
14
+
15
+ export const PANEL_APP_REFERENCE_TRIGGER_SPEC: ChatInputSurfaceTriggerSpec = {
16
+ key: 'panel-app-reference',
17
+ marker: '@',
18
+ };
19
+
20
+ function getPanelAppActivityTime(entry: PanelAppEntryView): number {
21
+ return Date.parse(entry.lastOpenedAt ?? entry.updatedAt) || 0;
22
+ }
23
+
24
+ function buildPanelAppReferenceItems(params: {
25
+ entries: readonly PanelAppEntryView[];
26
+ query: string;
27
+ texts: {
28
+ appIdLabel: string;
29
+ fileLabel: string;
30
+ noDescriptionLabel: string;
31
+ subtitle: string;
32
+ };
33
+ }): ChatInputSurfaceItem[] {
34
+ const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
35
+ return params.entries
36
+ .map((entry, order) => ({
37
+ entry,
38
+ order,
39
+ score: scoreInputSurfaceSearchCandidate(
40
+ {
41
+ id: entry.appId,
42
+ label: entry.title || entry.appId,
43
+ description: entry.description,
44
+ aliases: [entry.id, entry.fileName],
45
+ },
46
+ params.query,
47
+ ),
48
+ }))
49
+ .filter((entry) => entry.score > 0)
50
+ .sort((left, right) => {
51
+ const leftTier = resolveInputSurfaceMatchTier(left.score);
52
+ const rightTier = resolveInputSurfaceMatchTier(right.score);
53
+ if (rightTier !== leftTier) {
54
+ return rightTier - leftTier;
55
+ }
56
+ if (left.entry.favorite !== right.entry.favorite) {
57
+ return left.entry.favorite ? -1 : 1;
58
+ }
59
+ const rightActivity = getPanelAppActivityTime(right.entry);
60
+ const leftActivity = getPanelAppActivityTime(left.entry);
61
+ if (rightActivity !== leftActivity) {
62
+ return rightActivity - leftActivity;
63
+ }
64
+ if (right.score !== left.score) {
65
+ return right.score - left.score;
66
+ }
67
+ const labelCompare = collator.compare(left.entry.title || left.entry.appId, right.entry.title || right.entry.appId);
68
+ return labelCompare || left.order - right.order;
69
+ })
70
+ .map(({ entry }) => ({
71
+ key: `panel-app:${entry.appId}`,
72
+ title: entry.title || entry.appId,
73
+ subtitle: params.texts.subtitle,
74
+ description: (entry.description ?? '').trim() || params.texts.noDescriptionLabel,
75
+ detailLines: [
76
+ `${params.texts.appIdLabel}: ${entry.appId}`,
77
+ `${params.texts.fileLabel}: ${entry.fileName}`,
78
+ ],
79
+ tokenKind: 'panel_app',
80
+ tokenKey: entry.appId,
81
+ value: entry.appId,
82
+ }));
83
+ }
84
+
85
+ export function createPanelAppReferenceInputSurfacePlugin(params: {
86
+ itemTexts: {
87
+ appIdLabel: string;
88
+ fileLabel: string;
89
+ noDescriptionLabel: string;
90
+ subtitle: string;
91
+ };
92
+ menuTexts: ChatInputSurfaceMenuTexts;
93
+ }): ChatInputSurfacePlugin<ChatInputProductPluginData> {
94
+ return createInputSurfaceReferenceTokenPlugin({
95
+ key: 'panel-app-reference',
96
+ trigger: PANEL_APP_REFERENCE_TRIGGER_SPEC,
97
+ tokenKind: 'panel_app',
98
+ texts: params.menuTexts,
99
+ getIsLoading: ({ data }) => data.isPanelAppsLoading,
100
+ getRecords: ({ data }) => data.panelApps,
101
+ getItems: ({ records, query }) =>
102
+ buildPanelAppReferenceItems({
103
+ entries: records,
104
+ query,
105
+ texts: params.itemTexts,
106
+ }),
107
+ });
108
+ }
@@ -0,0 +1,161 @@
1
+ import {
2
+ CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC,
3
+ createInputSurfaceTriggeredPanelPlugin,
4
+ type ChatInputSurfaceItem,
5
+ type ChatInputSurfaceMenuTexts,
6
+ type ChatInputSurfacePlugin,
7
+ } from '@nextclaw/agent-chat-ui';
8
+ import { buildChatSlashItems } from '@/features/chat/features/input/utils/chat-input-bar.utils';
9
+ import type { ChatInputProductPluginData } from './chat-input-product-plugin-adapters.types';
10
+ import type { ChatInputBarAdapterTexts } from '@/features/chat/types/chat-input-bar.types';
11
+ import {
12
+ resolveInputSurfaceMatchTier,
13
+ scoreInputSurfaceSearchCandidate,
14
+ } from './input-surface-search.utils';
15
+
16
+ const SLASH_COMMAND_SECTION_KEY = 'commands';
17
+ const SLASH_SKILL_SECTION_KEY = 'skills';
18
+
19
+ export type ChatSlashCommandDescriptor = {
20
+ key: string;
21
+ title: string;
22
+ description: string;
23
+ detailLines?: readonly string[];
24
+ keywords?: readonly string[];
25
+ onSelect: () => void;
26
+ };
27
+
28
+ function buildSlashCommandItems(params: {
29
+ commands: readonly ChatSlashCommandDescriptor[];
30
+ query: string;
31
+ texts: {
32
+ commandHintLabel: string;
33
+ commandSubtitle: string;
34
+ commandSectionLabel: string;
35
+ };
36
+ }): ChatInputSurfaceItem[] {
37
+ const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
38
+ return params.commands
39
+ .map((command, order) => ({
40
+ command,
41
+ order,
42
+ score: scoreInputSurfaceSearchCandidate(
43
+ {
44
+ id: command.key,
45
+ label: command.title,
46
+ description: command.description,
47
+ aliases: command.keywords,
48
+ },
49
+ params.query,
50
+ ),
51
+ }))
52
+ .filter((entry) => entry.score > 0)
53
+ .sort((left, right) => {
54
+ const leftTier = resolveInputSurfaceMatchTier(left.score);
55
+ const rightTier = resolveInputSurfaceMatchTier(right.score);
56
+ if (rightTier !== leftTier) {
57
+ return rightTier - leftTier;
58
+ }
59
+ if (right.score !== left.score) {
60
+ return right.score - left.score;
61
+ }
62
+ return collator.compare(left.command.title, right.command.title) || left.order - right.order;
63
+ })
64
+ .map(({ command }) => ({
65
+ key: `command:${command.key}`,
66
+ title: command.title,
67
+ subtitle: params.texts.commandSubtitle,
68
+ description: command.description,
69
+ detailLines: [...(command.detailLines ?? [])],
70
+ hintLabel: params.texts.commandHintLabel,
71
+ sectionKey: SLASH_COMMAND_SECTION_KEY,
72
+ sectionLabel: params.texts.commandSectionLabel,
73
+ }));
74
+ }
75
+
76
+ function buildSlashSkillItems(params: {
77
+ data: ChatInputProductPluginData;
78
+ itemTexts: Pick<
79
+ ChatInputBarAdapterTexts,
80
+ 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'slashSkillScopeLabel' | 'noSkillDescription'
81
+ >;
82
+ skillHintLabel: string;
83
+ skillSectionLabel: string;
84
+ query: string;
85
+ }): ChatInputSurfaceItem[] {
86
+ const {
87
+ data,
88
+ itemTexts,
89
+ query,
90
+ skillHintLabel,
91
+ skillSectionLabel,
92
+ } = params;
93
+ return buildChatSlashItems(
94
+ [...data.skillRecords],
95
+ query,
96
+ itemTexts,
97
+ [...data.recentSkillValues],
98
+ ).map((item) => ({
99
+ ...item,
100
+ hintLabel: skillHintLabel,
101
+ sectionKey: SLASH_SKILL_SECTION_KEY,
102
+ sectionLabel: skillSectionLabel,
103
+ }));
104
+ }
105
+
106
+ export function createSlashCommandInputSurfacePlugin(params: {
107
+ commands: readonly ChatSlashCommandDescriptor[];
108
+ menuTexts: ChatInputSurfaceMenuTexts;
109
+ itemTexts: Pick<
110
+ ChatInputBarAdapterTexts,
111
+ 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'slashSkillScopeLabel' | 'noSkillDescription'
112
+ >;
113
+ labels: {
114
+ commandHintLabel: string;
115
+ commandSectionLabel: string;
116
+ commandSubtitle: string;
117
+ skillHintLabel: string;
118
+ skillSectionLabel: string;
119
+ };
120
+ onSelectSkill?: (skillRef: string) => void;
121
+ }): ChatInputSurfacePlugin<ChatInputProductPluginData> {
122
+ return createInputSurfaceTriggeredPanelPlugin({
123
+ key: 'slash-command',
124
+ trigger: CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC,
125
+ resolvePanel: (context) => {
126
+ const { data, trigger } = context;
127
+ return {
128
+ isLoading: data.isSkillsLoading,
129
+ items: [
130
+ ...buildSlashCommandItems({
131
+ commands: params.commands,
132
+ query: trigger.query,
133
+ texts: {
134
+ commandHintLabel: params.labels.commandHintLabel,
135
+ commandSectionLabel: params.labels.commandSectionLabel,
136
+ commandSubtitle: params.labels.commandSubtitle,
137
+ },
138
+ }),
139
+ ...buildSlashSkillItems({
140
+ data,
141
+ itemTexts: params.itemTexts,
142
+ query: trigger.query,
143
+ skillHintLabel: params.labels.skillHintLabel,
144
+ skillSectionLabel: params.labels.skillSectionLabel,
145
+ }),
146
+ ],
147
+ onSelectItem: (item) => {
148
+ if (item.key.startsWith('command:')) {
149
+ params.commands.find((command) => item.key === `command:${command.key}`)?.onSelect();
150
+ return;
151
+ }
152
+ const skillRef = item.tokenKey ?? item.value;
153
+ if (skillRef) {
154
+ params.onSelectSkill?.(skillRef);
155
+ }
156
+ },
157
+ texts: params.menuTexts,
158
+ };
159
+ },
160
+ });
161
+ }
@@ -1,5 +1,5 @@
1
1
  import { createChatComposerTextNode, createChatComposerTokenNode } from '@nextclaw/agent-chat-ui';
2
- import { deriveNcpMessagePartsFromComposer } from '../chat-composer-state.utils';
2
+ import { deriveNcpMessagePartsFromComposer } from '@/features/chat/features/input/utils/chat-composer-state.utils';
3
3
 
4
4
  describe('deriveNcpMessagePartsFromComposer', () => {
5
5
  it('preserves interleaved text and image token order while serializing skill tokens inline', () => {
@@ -68,6 +68,27 @@ describe('deriveNcpMessagePartsFromComposer', () => {
68
68
  ]);
69
69
  });
70
70
 
71
+ it('serializes panel app tokens as inline panel app references', () => {
72
+ expect(
73
+ deriveNcpMessagePartsFromComposer(
74
+ [
75
+ createChatComposerTextNode('review '),
76
+ createChatComposerTokenNode({
77
+ tokenKind: 'panel_app',
78
+ tokenKey: 'task-board',
79
+ label: 'Task Board'
80
+ })
81
+ ],
82
+ []
83
+ )
84
+ ).toEqual([
85
+ {
86
+ type: 'text',
87
+ text: 'review @panel-app:task-board'
88
+ }
89
+ ]);
90
+ });
91
+
71
92
  it('preserves uploaded attachment references when the attachment has a server uri', () => {
72
93
  const parts = deriveNcpMessagePartsFromComposer(
73
94
  [
@@ -1,10 +1,11 @@
1
1
  import { createChatComposerTextNode, createChatComposerTokenNode } from '@nextclaw/agent-chat-ui';
2
2
  import {
3
+ buildInlineTokensFromTextProtocol,
3
4
  buildInlineSkillTokensFromComposer,
4
5
  CHAT_UI_INLINE_TOKENS_METADATA_KEY,
5
6
  readInlineTokensFromMetadata,
6
- splitTextByInlineTokens
7
- } from '../chat-inline-token.utils';
7
+ resolveInlineTokensForText
8
+ } from '@/features/chat/features/input/utils/chat-inline-token.utils';
8
9
 
9
10
  describe('chat-inline-token utils', () => {
10
11
  it('builds ordered inline skill tokens from composer nodes', () => {
@@ -60,9 +61,9 @@ describe('chat-inline-token utils', () => {
60
61
  ]);
61
62
  });
62
63
 
63
- it('splits text into inline text and token fragments', () => {
64
+ it('merges metadata tokens with pure text protocol tokens', () => {
64
65
  expect(
65
- splitTextByInlineTokens('please use $weather now', [
66
+ resolveInlineTokensForText('please use $weather and @panel-app:task-board', [
66
67
  {
67
68
  kind: 'skill',
68
69
  key: 'weather',
@@ -71,17 +72,29 @@ describe('chat-inline-token utils', () => {
71
72
  }
72
73
  ])
73
74
  ).toEqual([
74
- { type: 'text', text: 'please use ' },
75
75
  {
76
- type: 'token',
77
- token: {
78
- kind: 'skill',
79
- key: 'weather',
80
- label: 'Weather',
81
- rawText: '$weather'
82
- }
76
+ kind: 'skill',
77
+ key: 'weather',
78
+ label: 'Weather',
79
+ rawText: '$weather'
83
80
  },
84
- { type: 'text', text: ' now' }
81
+ {
82
+ kind: 'panel_app',
83
+ key: 'task-board',
84
+ label: 'task-board',
85
+ rawText: '@panel-app:task-board'
86
+ }
87
+ ]);
88
+ });
89
+
90
+ it('builds inline panel app tokens from pure text protocol', () => {
91
+ expect(buildInlineTokensFromTextProtocol('review @panel-app:task-board now')).toEqual([
92
+ {
93
+ kind: 'panel_app',
94
+ key: 'task-board',
95
+ label: 'task-board',
96
+ rawText: '@panel-app:task-board'
97
+ }
85
98
  ]);
86
99
  });
87
100
  });