@nextclaw/ui 0.14.4 → 0.15.0

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 (194) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/assets/api-DLtsiBKm.js +13 -0
  3. package/dist/assets/appearance-settings-page-CtkeYXux.js +1 -0
  4. package/dist/assets/book-open-BQwWBDKb.js +1 -0
  5. package/dist/assets/channels-list-page-Cv8o9Hxg.js +8 -0
  6. package/dist/assets/chat-page-BtA7H1Ea.js +1 -0
  7. package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-EidRBpP2.js} +1 -1
  8. package/dist/assets/confirm-dialog-iIJFFp2T.js +5 -0
  9. package/dist/assets/desktop-update-config-C6n_TB1r.js +1 -0
  10. package/dist/assets/{dist-CikkhOlq.js → dist-CBxWOazz.js} +1 -1
  11. package/dist/assets/{dist-PVA_xL1D.js → dist-D2eZC3OR.js} +1 -1
  12. package/dist/assets/doc-browser-B76XOXmW.js +1 -0
  13. package/dist/assets/doc-browser-DBnj9UMB.js +1 -0
  14. package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-Sv-044Cl.js} +1 -1
  15. package/dist/assets/{ellipsis-y6vDtq4M.js → ellipsis-2uzhypw5.js} +1 -1
  16. package/dist/assets/es2015-DhQo_oS6.js +41 -0
  17. package/dist/assets/{external-link-DVJIQf_G.js → external-link-CM5dnUI6.js} +1 -1
  18. package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
  19. package/dist/assets/i18n-provider-di1waTGX.js +1 -0
  20. package/dist/assets/index-CBRP-7-a.css +1 -0
  21. package/dist/assets/index-Z1fYpbNd.js +104 -0
  22. package/dist/assets/loader-circle-BjmN5DeJ.js +1 -0
  23. package/dist/assets/mcp-marketplace-page-C_MVQUbT.js +9 -0
  24. package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +1 -0
  25. package/dist/assets/model-config-page-Cdr9P6TQ.js +1 -0
  26. package/dist/assets/plus-3ztRZD_l.js +1 -0
  27. package/dist/assets/provider-scoped-model-input-g1WkGiJ8.js +1 -0
  28. package/dist/assets/providers-config-page-CWlO7J4A.js +1 -0
  29. package/dist/assets/{react-VNqT5Zp7.js → react-DjpzoZXB.js} +1 -1
  30. package/dist/assets/refresh-cw-DaTv3zxE.js +1 -0
  31. package/dist/assets/remote-CKWZfVVB.js +1 -0
  32. package/dist/assets/rotate-cw-CHcaRUDz.js +1 -0
  33. package/dist/assets/runtime-config-page-CfTqh1nv.js +1 -0
  34. package/dist/assets/{save-BOJ477H-.js → save-BZof1uie.js} +1 -1
  35. package/dist/assets/{search-BEti5beA.js → search-MxAarS_g.js} +1 -1
  36. package/dist/assets/search-config-page-Dm8daYny.js +1 -0
  37. package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-D8AbcR05.js} +2 -2
  38. package/dist/assets/security-config-Ds0LsJmo.js +1 -0
  39. package/dist/assets/select-D7GcNP0z.js +1 -0
  40. package/dist/assets/setting-row-CuozlnJd.js +1 -0
  41. package/dist/assets/side-dock-BcLau5S_.js +3 -0
  42. package/dist/assets/skeleton-shfmXuYp.js +1 -0
  43. package/dist/assets/switch-C1BWMU5c.js +1 -0
  44. package/dist/assets/tag-chip-DCUrQUCP.js +1 -0
  45. package/dist/assets/tooltip-DfAHHA76.js +1 -0
  46. package/dist/assets/use-config-DAdbT5QJ.js +1 -0
  47. package/dist/assets/use-confirm-dialog-eWcJ6V-s.js +1 -0
  48. package/dist/assets/x-DqL1mYLC.js +1 -0
  49. package/dist/index.html +53 -17
  50. package/index.html +25 -2
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
  53. package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
  54. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
  55. package/src/app/components/layout/app-layout.tsx +10 -3
  56. package/src/app/components/layout/sidebar-items.tsx +8 -8
  57. package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
  58. package/src/app/components/layout/sidebar.tsx +21 -28
  59. package/src/app/configs/app-navigation.config.ts +11 -5
  60. package/src/app/index.tsx +8 -0
  61. package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
  62. package/src/app/managers/viewport-layout.manager.ts +39 -0
  63. package/src/app/presenters/app.presenter.ts +25 -2
  64. package/src/app/styles/design-system.css +91 -4
  65. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
  66. package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
  67. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
  68. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
  69. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
  70. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +51 -2
  71. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
  72. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
  73. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
  74. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
  75. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
  76. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
  77. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
  78. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
  79. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
  80. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
  81. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
  82. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
  83. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +166 -0
  84. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
  85. package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
  86. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
  87. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
  88. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
  89. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
  90. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
  91. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
  92. package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
  93. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +39 -0
  94. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  95. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
  96. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
  97. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +270 -115
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +70 -5
  102. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
  103. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +79 -1
  104. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
  105. package/src/features/chat/managers/chat-thread.manager.ts +9 -3
  106. package/src/features/chat/managers/chat-ui.manager.ts +5 -1
  107. package/src/features/chat/presenters/chat.presenter.ts +2 -1
  108. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  109. package/src/features/chat/stores/chat-thread.store.ts +6 -64
  110. package/src/features/chat/types/chat-message.types.ts +4 -0
  111. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
  112. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
  113. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  114. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
  115. package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
  116. package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
  117. package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
  118. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
  119. package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
  120. package/src/features/marketplace/index.ts +3 -0
  121. package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
  122. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
  123. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
  124. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
  125. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
  126. package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
  127. package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
  128. package/src/features/side-dock/components/side-dock.tsx +100 -13
  129. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
  130. package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
  131. package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
  132. package/src/features/side-dock/stores/side-dock.store.ts +9 -0
  133. package/src/features/side-dock/types/side-dock.types.ts +10 -3
  134. package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
  135. package/src/index.css +501 -27
  136. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  137. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
  138. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
  139. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
  140. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
  141. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
  142. package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
  143. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
  144. package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
  145. package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
  146. package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
  147. package/src/shared/components/ui/dialog.tsx +2 -2
  148. package/src/shared/components/ui/popover.tsx +1 -1
  149. package/src/shared/components/ui/select.tsx +1 -1
  150. package/src/shared/components/ui/switch.tsx +4 -3
  151. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
  152. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
  153. package/src/shared/components/ui/tooltip.tsx +1 -1
  154. package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
  155. package/src/shared/lib/i18n/locales/en-US/chat.json +15 -6
  156. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  157. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
  158. package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
  159. package/src/shared/lib/i18n/locales/zh-CN/chat.json +15 -6
  160. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  161. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
  162. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
  163. package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
  164. package/src/shared/lib/session-project/index.ts +1 -1
  165. package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
  166. package/src/shared/lib/theme/index.ts +51 -13
  167. package/dist/assets/api-BP_8IaxB.js +0 -13
  168. package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
  169. package/dist/assets/chat-page-BRB0N5WW.js +0 -105
  170. package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
  171. package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
  172. package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
  173. package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
  174. package/dist/assets/index-BiE-K8-q.css +0 -1
  175. package/dist/assets/index-D9BAE_L_.js +0 -105
  176. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
  177. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
  178. package/dist/assets/model-config-page-DotaTUHb.js +0 -1
  179. package/dist/assets/plus-BbZqquJe.js +0 -1
  180. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
  181. package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
  182. package/dist/assets/remote-B8TiUfwW.js +0 -1
  183. package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
  184. package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
  185. package/dist/assets/search-config-page-DGj5am03.js +0 -1
  186. package/dist/assets/select-hoj8dK4K.js +0 -41
  187. package/dist/assets/tag-chip-T8-h-die.js +0 -1
  188. package/dist/assets/use-config-CjRV2hwL.js +0 -1
  189. package/dist/assets/x-D9PXs_xP.js +0 -1
  190. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
  191. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
  192. package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
  193. /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
  194. /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
@@ -1,8 +1,36 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import { resolveChatInputSurfaceState } from '@nextclaw/agent-chat-ui';
3
3
  import { createSlashCommandInputSurfacePlugin } from '@/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils';
4
+ import type { PanelAppEntryView } from '@/shared/lib/api';
4
5
 
5
- function createPlugin(onSelectCommand = vi.fn()) {
6
+ function createPanelApp(
7
+ overrides: Partial<PanelAppEntryView> & Pick<PanelAppEntryView, 'appId' | 'title'>,
8
+ ): PanelAppEntryView {
9
+ return {
10
+ id: overrides.id ?? overrides.appId,
11
+ appId: overrides.appId,
12
+ fileName: overrides.fileName ?? `${overrides.appId}.panel.html`,
13
+ kind: overrides.kind ?? 'single-file',
14
+ title: overrides.title,
15
+ description: overrides.description,
16
+ contentPath: overrides.contentPath ?? `/panels/${overrides.appId}.panel.html`,
17
+ createdAt: overrides.createdAt ?? '2026-06-18T00:00:00.000Z',
18
+ updatedAt: overrides.updatedAt ?? '2026-06-18T00:00:00.000Z',
19
+ sizeBytes: overrides.sizeBytes ?? 100,
20
+ favorite: overrides.favorite ?? false,
21
+ clientDeclared: overrides.clientDeclared ?? false,
22
+ clientGranted: overrides.clientGranted ?? false,
23
+ lastOpenedAt: overrides.lastOpenedAt,
24
+ openCount: overrides.openCount ?? 0,
25
+ };
26
+ }
27
+
28
+ function createPlugin(
29
+ params: {
30
+ onSelectCommand?: () => void;
31
+ onSelectPanelApp?: (appId: string) => void;
32
+ } = {},
33
+ ) {
6
34
  return createSlashCommandInputSurfacePlugin({
7
35
  commands: [
8
36
  {
@@ -11,19 +39,33 @@ function createPlugin(onSelectCommand = vi.fn()) {
11
39
  description: 'Open side chat',
12
40
  detailLines: ['Creates a child session on first send'],
13
41
  keywords: ['side', 'chat'],
14
- onSelect: onSelectCommand,
42
+ onSelect: params.onSelectCommand ?? vi.fn(),
15
43
  },
16
44
  ],
17
45
  itemTexts: {
18
- noSkillDescription: 'No description',
19
- slashSkillScopeLabel: 'Scope',
20
- slashSkillSpecLabel: 'Spec',
21
- slashSkillSubtitle: 'Skill',
46
+ panelAppTexts: {
47
+ appIdLabel: 'App ID',
48
+ fileLabel: 'File',
49
+ noDescriptionLabel: 'No panel app description',
50
+ subtitle: 'Panel App',
51
+ },
52
+ skillTexts: {
53
+ noSkillDescription: 'No description',
54
+ slashSkillScopeLabel: 'Scope',
55
+ slashSkillSpecLabel: 'Spec',
56
+ slashSkillSubtitle: 'Skill',
57
+ },
22
58
  },
23
59
  labels: {
24
60
  commandHintLabel: 'Run command',
25
61
  commandSectionLabel: 'Commands',
26
62
  commandSubtitle: 'Command',
63
+ filterAllLabel: 'All',
64
+ filterCommandsLabel: 'Commands',
65
+ filterPanelAppsLabel: 'Panel Apps',
66
+ filterSkillsLabel: 'Skills',
67
+ panelAppHintLabel: 'Open panel app',
68
+ panelAppSectionLabel: 'Panel Apps',
27
69
  skillHintLabel: 'Add skill',
28
70
  skillSectionLabel: 'Skills',
29
71
  },
@@ -34,6 +76,7 @@ function createPlugin(onSelectCommand = vi.fn()) {
34
76
  loadingLabel: 'Loading',
35
77
  sectionLabel: 'Slash',
36
78
  },
79
+ onSelectPanelApp: params.onSelectPanelApp ?? vi.fn(),
37
80
  onSelectSkill: vi.fn(),
38
81
  });
39
82
  }
@@ -44,7 +87,13 @@ describe('createSlashCommandInputSurfacePlugin', () => {
44
87
  data: {
45
88
  isPanelAppsLoading: false,
46
89
  isSkillsLoading: false,
47
- panelApps: [],
90
+ panelApps: [
91
+ createPanelApp({
92
+ appId: 'task-board',
93
+ title: 'Task Board',
94
+ description: 'Track tasks',
95
+ }),
96
+ ],
48
97
  recentSkillValues: [],
49
98
  skillRecords: [
50
99
  {
@@ -67,6 +116,13 @@ describe('createSlashCommandInputSurfacePlugin', () => {
67
116
  expect(state.panel?.items.map((item) => item.key)).toEqual([
68
117
  'command:side-chat',
69
118
  'skill:web-search',
119
+ 'panel-app-action:task-board',
120
+ ]);
121
+ expect(state.panel?.filterOptions).toEqual([
122
+ { key: 'all', label: 'All' },
123
+ { key: 'commands', label: 'Commands', sectionKeys: ['commands'] },
124
+ { key: 'skills', label: 'Skills', sectionKeys: ['skills'] },
125
+ { key: 'panel-apps', label: 'Panel Apps', sectionKeys: ['panel-apps'] },
70
126
  ]);
71
127
  expect(state.panel?.items[0]).toMatchObject({
72
128
  sectionKey: 'commands',
@@ -78,6 +134,11 @@ describe('createSlashCommandInputSurfacePlugin', () => {
78
134
  sectionLabel: 'Skills',
79
135
  hintLabel: 'Add skill',
80
136
  });
137
+ expect(state.panel?.items[2]).toMatchObject({
138
+ sectionKey: 'panel-apps',
139
+ sectionLabel: 'Panel Apps',
140
+ hintLabel: 'Open panel app',
141
+ });
81
142
  });
82
143
 
83
144
  it('runs a command item through the panel selection handler', () => {
@@ -90,7 +151,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
90
151
  recentSkillValues: [],
91
152
  skillRecords: [],
92
153
  },
93
- plugins: [createPlugin(onSelectCommand)],
154
+ plugins: [createPlugin({ onSelectCommand })],
94
155
  trigger: {
95
156
  key: 'slash',
96
157
  marker: '/',
@@ -105,4 +166,43 @@ describe('createSlashCommandInputSurfacePlugin', () => {
105
166
  state.panel?.onSelectItem?.(item!);
106
167
  expect(onSelectCommand).toHaveBeenCalledTimes(1);
107
168
  });
169
+
170
+ it('opens a panel app action without turning it into an input token', () => {
171
+ const onSelectPanelApp = vi.fn();
172
+ const state = resolveChatInputSurfaceState({
173
+ data: {
174
+ isPanelAppsLoading: false,
175
+ isSkillsLoading: false,
176
+ panelApps: [
177
+ createPanelApp({
178
+ appId: 'task-board',
179
+ title: 'Task Board',
180
+ description: 'Track tasks',
181
+ }),
182
+ ],
183
+ recentSkillValues: [],
184
+ skillRecords: [],
185
+ },
186
+ plugins: [createPlugin({ onSelectPanelApp })],
187
+ trigger: {
188
+ key: 'slash',
189
+ marker: '/',
190
+ query: 'task',
191
+ start: 0,
192
+ end: 5,
193
+ },
194
+ });
195
+
196
+ const item = state.panel?.items[0];
197
+ expect(item).toMatchObject({
198
+ key: 'panel-app-action:task-board',
199
+ title: 'Task Board',
200
+ sectionKey: 'panel-apps',
201
+ });
202
+ expect(item).not.toHaveProperty('tokenKind');
203
+ expect(item).not.toHaveProperty('tokenKey');
204
+ expect(item).not.toHaveProperty('value');
205
+ state.panel?.onSelectItem?.(item!);
206
+ expect(onSelectPanelApp).toHaveBeenCalledWith('task-board');
207
+ });
108
208
  });
@@ -21,16 +21,17 @@ function getPanelAppActivityTime(entry: PanelAppEntryView): number {
21
21
  return Date.parse(entry.lastOpenedAt ?? entry.updatedAt) || 0;
22
22
  }
23
23
 
24
- function buildPanelAppReferenceItems(params: {
24
+ export type PanelAppInputSurfaceItemTexts = {
25
+ appIdLabel: string;
26
+ fileLabel: string;
27
+ noDescriptionLabel: string;
28
+ subtitle: string;
29
+ };
30
+
31
+ function resolvePanelAppInputSurfaceEntries(params: {
25
32
  entries: readonly PanelAppEntryView[];
26
33
  query: string;
27
- texts: {
28
- appIdLabel: string;
29
- fileLabel: string;
30
- noDescriptionLabel: string;
31
- subtitle: string;
32
- };
33
- }): ChatInputSurfaceItem[] {
34
+ }): PanelAppEntryView[] {
34
35
  const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
35
36
  return params.entries
36
37
  .map((entry, order) => ({
@@ -67,8 +68,23 @@ function buildPanelAppReferenceItems(params: {
67
68
  const labelCompare = collator.compare(left.entry.title || left.entry.appId, right.entry.title || right.entry.appId);
68
69
  return labelCompare || left.order - right.order;
69
70
  })
70
- .map(({ entry }) => ({
71
- key: `panel-app:${entry.appId}`,
71
+ .map(({ entry }) => entry);
72
+ }
73
+
74
+ function buildPanelAppInputSurfaceItemEntries(params: {
75
+ entries: readonly PanelAppEntryView[];
76
+ keyPrefix?: string;
77
+ query: string;
78
+ texts: PanelAppInputSurfaceItemTexts;
79
+ }): Array<{ entry: PanelAppEntryView; item: ChatInputSurfaceItem }> {
80
+ const keyPrefix = params.keyPrefix ?? 'panel-app';
81
+ return resolvePanelAppInputSurfaceEntries({
82
+ entries: params.entries,
83
+ query: params.query,
84
+ }).map((entry) => ({
85
+ entry,
86
+ item: {
87
+ key: `${keyPrefix}:${entry.appId}`,
72
88
  title: entry.title || entry.appId,
73
89
  subtitle: params.texts.subtitle,
74
90
  description: (entry.description ?? '').trim() || params.texts.noDescriptionLabel,
@@ -76,10 +92,30 @@ function buildPanelAppReferenceItems(params: {
76
92
  `${params.texts.appIdLabel}: ${entry.appId}`,
77
93
  `${params.texts.fileLabel}: ${entry.fileName}`,
78
94
  ],
79
- tokenKind: 'panel_app',
80
- tokenKey: entry.appId,
81
- value: entry.appId,
82
- }));
95
+ },
96
+ }));
97
+ }
98
+
99
+ export function buildPanelAppInputSurfaceItems(params: {
100
+ entries: readonly PanelAppEntryView[];
101
+ keyPrefix?: string;
102
+ query: string;
103
+ texts: PanelAppInputSurfaceItemTexts;
104
+ }): ChatInputSurfaceItem[] {
105
+ return buildPanelAppInputSurfaceItemEntries(params).map(({ item }) => item);
106
+ }
107
+
108
+ function buildPanelAppReferenceItems(params: {
109
+ entries: readonly PanelAppEntryView[];
110
+ query: string;
111
+ texts: PanelAppInputSurfaceItemTexts;
112
+ }): ChatInputSurfaceItem[] {
113
+ return buildPanelAppInputSurfaceItemEntries(params).map(({ entry, item }) => ({
114
+ ...item,
115
+ tokenKind: 'panel_app',
116
+ tokenKey: entry.appId,
117
+ value: entry.appId,
118
+ }));
83
119
  }
84
120
 
85
121
  export function createPanelAppReferenceInputSurfacePlugin(params: {
@@ -8,12 +8,18 @@ import {
8
8
  import { buildChatSlashItems } from '@/features/chat/features/input/utils/chat-input-bar.utils';
9
9
  import type { ChatInputProductPluginData } from './chat-input-product-plugin-adapters.types';
10
10
  import type { ChatInputBarAdapterTexts } from '@/features/chat/types/chat-input-bar.types';
11
+ import {
12
+ buildPanelAppInputSurfaceItems,
13
+ type PanelAppInputSurfaceItemTexts,
14
+ } from './panel-app-reference-plugin.utils';
11
15
  import {
12
16
  resolveInputSurfaceMatchTier,
13
17
  scoreInputSurfaceSearchCandidate,
14
18
  } from './input-surface-search.utils';
15
19
 
16
20
  const SLASH_COMMAND_SECTION_KEY = 'commands';
21
+ const SLASH_PANEL_APP_KEY_PREFIX = 'panel-app-action';
22
+ const SLASH_PANEL_APP_SECTION_KEY = 'panel-apps';
17
23
  const SLASH_SKILL_SECTION_KEY = 'skills';
18
24
 
19
25
  export type ChatSlashCommandDescriptor = {
@@ -103,20 +109,55 @@ function buildSlashSkillItems(params: {
103
109
  }));
104
110
  }
105
111
 
112
+ function buildSlashPanelAppItems(params: {
113
+ data: ChatInputProductPluginData;
114
+ itemTexts: PanelAppInputSurfaceItemTexts;
115
+ panelAppHintLabel: string;
116
+ panelAppSectionLabel: string;
117
+ query: string;
118
+ }): ChatInputSurfaceItem[] {
119
+ return buildPanelAppInputSurfaceItems({
120
+ entries: params.data.panelApps,
121
+ keyPrefix: SLASH_PANEL_APP_KEY_PREFIX,
122
+ query: params.query,
123
+ texts: params.itemTexts,
124
+ }).map((item) => ({
125
+ ...item,
126
+ hintLabel: params.panelAppHintLabel,
127
+ sectionKey: SLASH_PANEL_APP_SECTION_KEY,
128
+ sectionLabel: params.panelAppSectionLabel,
129
+ }));
130
+ }
131
+
132
+ function resolveSlashPanelAppId(item: ChatInputSurfaceItem): string | null {
133
+ const prefix = `${SLASH_PANEL_APP_KEY_PREFIX}:`;
134
+ return item.key.startsWith(prefix) ? item.key.slice(prefix.length) : null;
135
+ }
136
+
106
137
  export function createSlashCommandInputSurfacePlugin(params: {
107
138
  commands: readonly ChatSlashCommandDescriptor[];
108
139
  menuTexts: ChatInputSurfaceMenuTexts;
109
- itemTexts: Pick<
110
- ChatInputBarAdapterTexts,
111
- 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'slashSkillScopeLabel' | 'noSkillDescription'
112
- >;
140
+ itemTexts: {
141
+ panelAppTexts: PanelAppInputSurfaceItemTexts;
142
+ skillTexts: Pick<
143
+ ChatInputBarAdapterTexts,
144
+ 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'slashSkillScopeLabel' | 'noSkillDescription'
145
+ >;
146
+ };
113
147
  labels: {
114
148
  commandHintLabel: string;
115
149
  commandSectionLabel: string;
116
150
  commandSubtitle: string;
151
+ filterAllLabel: string;
152
+ filterCommandsLabel: string;
153
+ filterPanelAppsLabel: string;
154
+ filterSkillsLabel: string;
155
+ panelAppHintLabel: string;
156
+ panelAppSectionLabel: string;
117
157
  skillHintLabel: string;
118
158
  skillSectionLabel: string;
119
159
  };
160
+ onSelectPanelApp: (appId: string) => void;
120
161
  onSelectSkill?: (skillRef: string) => void;
121
162
  }): ChatInputSurfacePlugin<ChatInputProductPluginData> {
122
163
  return createInputSurfaceTriggeredPanelPlugin({
@@ -125,7 +166,25 @@ export function createSlashCommandInputSurfacePlugin(params: {
125
166
  resolvePanel: (context) => {
126
167
  const { data, trigger } = context;
127
168
  return {
128
- isLoading: data.isSkillsLoading,
169
+ filterOptions: [
170
+ { key: 'all', label: params.labels.filterAllLabel },
171
+ {
172
+ key: SLASH_COMMAND_SECTION_KEY,
173
+ label: params.labels.filterCommandsLabel,
174
+ sectionKeys: [SLASH_COMMAND_SECTION_KEY],
175
+ },
176
+ {
177
+ key: SLASH_SKILL_SECTION_KEY,
178
+ label: params.labels.filterSkillsLabel,
179
+ sectionKeys: [SLASH_SKILL_SECTION_KEY],
180
+ },
181
+ {
182
+ key: SLASH_PANEL_APP_SECTION_KEY,
183
+ label: params.labels.filterPanelAppsLabel,
184
+ sectionKeys: [SLASH_PANEL_APP_SECTION_KEY],
185
+ },
186
+ ],
187
+ isLoading: data.isPanelAppsLoading || data.isSkillsLoading,
129
188
  items: [
130
189
  ...buildSlashCommandItems({
131
190
  commands: params.commands,
@@ -138,17 +197,29 @@ export function createSlashCommandInputSurfacePlugin(params: {
138
197
  }),
139
198
  ...buildSlashSkillItems({
140
199
  data,
141
- itemTexts: params.itemTexts,
200
+ itemTexts: params.itemTexts.skillTexts,
142
201
  query: trigger.query,
143
202
  skillHintLabel: params.labels.skillHintLabel,
144
203
  skillSectionLabel: params.labels.skillSectionLabel,
145
204
  }),
205
+ ...buildSlashPanelAppItems({
206
+ data,
207
+ itemTexts: params.itemTexts.panelAppTexts,
208
+ panelAppHintLabel: params.labels.panelAppHintLabel,
209
+ panelAppSectionLabel: params.labels.panelAppSectionLabel,
210
+ query: trigger.query,
211
+ }),
146
212
  ],
147
213
  onSelectItem: (item) => {
148
214
  if (item.key.startsWith('command:')) {
149
215
  params.commands.find((command) => item.key === `command:${command.key}`)?.onSelect();
150
216
  return;
151
217
  }
218
+ const panelAppId = resolveSlashPanelAppId(item);
219
+ if (panelAppId) {
220
+ params.onSelectPanelApp(panelAppId);
221
+ return;
222
+ }
152
223
  const skillRef = item.tokenKey ?? item.value;
153
224
  if (skillRef) {
154
225
  params.onSelectSkill?.(skillRef);
@@ -52,27 +52,19 @@ beforeEach(() => {
52
52
  mocks.handleIframeMessage.mockReset();
53
53
  mocks.open.mockReset();
54
54
  mocks.openTarget.mockReset();
55
+ mocks.panelApps.isLoading = false;
55
56
  });
56
57
 
57
58
  it('renders inline panel apps as bounded card-mode iframes with an expand action', () => {
58
59
  render(<ChatInlinePanelAppCard panelApp={{
59
- action: {
60
- kind: 'show-content',
61
- label: 'Show content',
62
- request: {
63
- placement: 'side_panel',
64
- target: {
65
- payload: { appId: 'weather-card' },
66
- type: 'panel_app',
67
- },
68
- title: 'Weather',
69
- },
70
- },
71
60
  appId: 'weather-card',
72
61
  title: 'Weather',
73
62
  }} />);
74
63
 
75
64
  const iframe = screen.getByTitle('Weather');
65
+ const card = iframe.closest('[data-chat-message-wide-content="true"]');
66
+ expect(card?.className).toContain('w-full');
67
+ expect(card?.className).toContain('max-w-[48rem]');
76
68
  expect(iframe.getAttribute('src')).toBe(
77
69
  '/api/panel-apps/weather-card/content?nextclawDisplayMode=card&nextclawPlacement=inline',
78
70
  );
@@ -87,3 +79,20 @@ it('renders inline panel apps as bounded card-mode iframes with an expand action
87
79
  url: '/api/panel-apps/weather-card/content',
88
80
  }));
89
81
  });
82
+
83
+ it('can render inline panel apps without a side-panel expand action', () => {
84
+ render(
85
+ <ChatInlinePanelAppCard
86
+ panelApp={{
87
+ appId: 'weather-card',
88
+ title: 'Weather',
89
+ }}
90
+ showExpandAction={false}
91
+ />,
92
+ );
93
+
94
+ expect(screen.getByTitle('Weather')).toBeTruthy();
95
+ expect(screen.queryByLabelText('chatPanelCardExpand')).toBeNull();
96
+ expect(mocks.open).not.toHaveBeenCalled();
97
+ expect(mocks.openTarget).not.toHaveBeenCalled();
98
+ });
@@ -1,4 +1,5 @@
1
1
  import { render, screen } from "@testing-library/react";
2
+ import { isValidElement } from "react";
2
3
  import type { NcpMessage } from "@nextclaw/ncp";
3
4
  import { beforeEach, expect, it, vi } from "vitest";
4
5
  import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
@@ -7,6 +8,8 @@ const captures = vi.hoisted(() => ({
7
8
  renders: [] as Array<{
8
9
  messages: unknown[];
9
10
  onToolAction?: (action: unknown) => void;
11
+ onFileOpen?: (action: unknown) => void;
12
+ renderInlineDisplay?: (display: unknown) => unknown;
10
13
  renderPanelAppCard?: (panelApp: unknown) => unknown;
11
14
  texts?: Record<string, unknown>;
12
15
  }>,
@@ -22,6 +25,8 @@ vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
22
25
  ChatMessageList: (props: {
23
26
  messages: unknown[];
24
27
  onToolAction?: (action: unknown) => void;
28
+ onFileOpen?: (action: unknown) => void;
29
+ renderInlineDisplay?: (display: unknown) => unknown;
25
30
  renderPanelAppCard?: (panelApp: unknown) => unknown;
26
31
  texts?: Record<string, unknown>;
27
32
  }) => {
@@ -171,6 +176,130 @@ it("adapts persisted inline token metadata into markdown token data", () => {
171
176
  });
172
177
  });
173
178
 
179
+ it("adds a completed assistant process summary without inventing a duration", () => {
180
+ const userMessage = {
181
+ id: "user-process-request",
182
+ sessionId: "session-1",
183
+ role: "user",
184
+ status: "final",
185
+ timestamp: "2026-03-31T10:00:00.000Z",
186
+ parts: [{ type: "text", text: "please inspect the repo" }],
187
+ } satisfies NcpMessage;
188
+ const assistantMessage = {
189
+ id: "assistant-process-result",
190
+ sessionId: "session-1",
191
+ role: "assistant",
192
+ status: "final",
193
+ timestamp: "2026-03-31T10:01:03.000Z",
194
+ parts: [
195
+ {
196
+ type: "reasoning",
197
+ text: "Inspecting current state.",
198
+ },
199
+ {
200
+ type: "tool-invocation",
201
+ toolCallId: "tool-1",
202
+ toolName: "exec_command",
203
+ state: "result",
204
+ args: "{\"cmd\":\"git status\"}",
205
+ result: "clean",
206
+ },
207
+ {
208
+ type: "text",
209
+ text: "Done.",
210
+ },
211
+ ],
212
+ } satisfies NcpMessage;
213
+
214
+ render(
215
+ <ChatMessageListContainer
216
+ messages={[userMessage, assistantMessage]}
217
+ isSending={false}
218
+ />,
219
+ );
220
+
221
+ const renderedMessages =
222
+ captures.renders[captures.renders.length - 1]?.messages ?? [];
223
+ expect(renderedMessages[1]).toMatchObject({
224
+ processSummary: {
225
+ label: "chatProcessSummaryProcessed",
226
+ },
227
+ });
228
+ });
229
+
230
+ it("derives completed assistant process duration from message lifecycle", () => {
231
+ const assistantMessage = {
232
+ id: "assistant-process-result",
233
+ sessionId: "session-1",
234
+ role: "assistant",
235
+ status: "final",
236
+ timestamp: "2026-03-31T10:01:03.000Z",
237
+ lifecycle: {
238
+ startedAt: "2026-03-31T10:00:00.000Z",
239
+ endedAt: "2026-03-31T10:03:51.000Z",
240
+ },
241
+ parts: [
242
+ {
243
+ type: "reasoning",
244
+ text: "Inspecting current state.",
245
+ },
246
+ {
247
+ type: "tool-invocation",
248
+ toolCallId: "tool-1",
249
+ toolName: "exec_command",
250
+ state: "result",
251
+ args: "{\"cmd\":\"git status\"}",
252
+ result: "clean",
253
+ },
254
+ {
255
+ type: "text",
256
+ text: "Done.",
257
+ },
258
+ ],
259
+ } satisfies NcpMessage;
260
+
261
+ render(
262
+ <ChatMessageListContainer
263
+ messages={[assistantMessage]}
264
+ isSending={false}
265
+ />,
266
+ );
267
+
268
+ const renderedMessages =
269
+ captures.renders[captures.renders.length - 1]?.messages ?? [];
270
+ expect(renderedMessages[0]).toMatchObject({
271
+ processSummary: {
272
+ label: "chatProcessSummaryProcessed 3m 51s",
273
+ },
274
+ });
275
+ });
276
+
277
+ it("wires markdown file link actions to the workspace file preview manager", () => {
278
+ const message = {
279
+ id: "assistant-file-link",
280
+ sessionId: "session-1",
281
+ role: "assistant",
282
+ status: "final",
283
+ timestamp: "2026-03-31T10:00:00.000Z",
284
+ parts: [
285
+ {
286
+ type: "text",
287
+ text: "[particle-cosmos.html](/Users/peiwang/Downloads/particle-cosmos.html)",
288
+ },
289
+ ],
290
+ } satisfies NcpMessage;
291
+ const action = {
292
+ path: "/Users/peiwang/Downloads/particle-cosmos.html",
293
+ label: "particle-cosmos.html",
294
+ viewMode: "preview",
295
+ };
296
+
297
+ render(<ChatMessageListContainer messages={[message]} isSending={false} />);
298
+ captures.renders[captures.renders.length - 1]?.onFileOpen?.(action);
299
+
300
+ expect(captures.openFilePreview).toHaveBeenCalledWith(action);
301
+ });
302
+
174
303
  it("renders context inheritance as a divider without repeating inherited messages", () => {
175
304
  const inheritedMessage = {
176
305
  id: "child-session:inherited:1",
@@ -294,6 +423,43 @@ it("passes the inline panel app renderer to the shared chat UI", () => {
294
423
  expect(captures.renders[captures.renders.length - 1]?.renderPanelAppCard).toEqual(expect.any(Function));
295
424
  });
296
425
 
426
+ it("passes the inline display renderer to the shared chat UI", () => {
427
+ render(<ChatMessageListContainer messages={[]} isSending={false} />);
428
+
429
+ const renderInlineDisplay =
430
+ captures.renders[captures.renders.length - 1]?.renderInlineDisplay;
431
+
432
+ expect(renderInlineDisplay).toEqual(expect.any(Function));
433
+ expect(
434
+ renderInlineDisplay?.({
435
+ target: {
436
+ type: "file",
437
+ payload: { path: "README.md" },
438
+ },
439
+ title: "README",
440
+ }),
441
+ ).toBeUndefined();
442
+ });
443
+
444
+ it("keeps inline panel app displays expandable from the card header", () => {
445
+ render(<ChatMessageListContainer messages={[]} isSending={false} />);
446
+
447
+ const renderInlineDisplay =
448
+ captures.renders[captures.renders.length - 1]?.renderInlineDisplay;
449
+ const rendered = renderInlineDisplay?.({
450
+ target: {
451
+ type: "panel_app",
452
+ payload: { appId: "weather-card" },
453
+ },
454
+ title: "Weather",
455
+ });
456
+
457
+ if (!isValidElement<{ showExpandAction?: boolean }>(rendered)) {
458
+ throw new Error("Expected inline panel app renderer to return a React element");
459
+ }
460
+ expect(rendered.props.showExpandAction).toBeUndefined();
461
+ });
462
+
297
463
  it("renders context compaction as an in-flow divider instead of a chat message", () => {
298
464
  const beforeMessage = {
299
465
  id: "message-before",