@nextclaw/ui 0.14.1 → 0.14.3

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 (224) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/{api-DPcvCLfa.js → api-BP_8IaxB.js} +7 -7
  3. package/dist/assets/channels-list-page-DEh5Jjqx.js +8 -0
  4. package/dist/assets/chat-page-BRB0N5WW.js +105 -0
  5. package/dist/assets/config-split-page-CRhlZ_pH.js +1 -0
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-DsivFJ4r.js} +2 -2
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-B0w9p2ep.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-CikkhOlq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.js → dist-PVA_xL1D.js} +1 -1
  10. package/dist/assets/doc-browser-BoNJ7-tE.js +1 -0
  11. package/dist/assets/doc-browser-CdJ_d4ik.js +1 -0
  12. package/dist/assets/doc-browser-context-MTw4EvKd.js +1 -0
  13. package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-y6vDtq4M.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.js → external-link-DVJIQf_G.js} +1 -1
  15. package/dist/assets/index-BiE-K8-q.css +1 -0
  16. package/dist/assets/index-D9BAE_L_.js +105 -0
  17. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-DotaTUHb.js +1 -0
  21. package/dist/assets/plus-BbZqquJe.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +1 -0
  23. package/dist/assets/providers-config-page-DW4tQSLN.js +1 -0
  24. package/dist/assets/{react-sZz5HSCm.js → react-VNqT5Zp7.js} +1 -1
  25. package/dist/assets/remote-B8TiUfwW.js +1 -0
  26. package/dist/assets/rotate-cw-DjHDTkCv.js +1 -0
  27. package/dist/assets/runtime-config-page-S6ErRHR8.js +1 -0
  28. package/dist/assets/{save-MzjoiHri.js → save-BOJ477H-.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BEti5beA.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-DGj5am03.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page--IzrIOid.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-hoj8dK4K.js} +2 -2
  33. package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-T8-h-die.js} +1 -1
  34. package/dist/assets/use-config-CjRV2hwL.js +1 -0
  35. package/dist/assets/x-D9PXs_xP.js +1 -0
  36. package/dist/index.html +70 -19
  37. package/index.html +55 -5
  38. package/package.json +9 -9
  39. package/src/app/components/app-presenter-provider.tsx +11 -2
  40. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  41. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  42. package/src/app/components/layout/app-layout.tsx +18 -4
  43. package/src/app/components/layout/header.tsx +3 -3
  44. package/src/app/components/layout/page-layout.tsx +2 -2
  45. package/src/app/components/layout/runtime-status-entry.tsx +7 -7
  46. package/src/app/components/layout/sidebar-items.tsx +194 -71
  47. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  48. package/src/app/components/layout/sidebar.tsx +340 -83
  49. package/src/app/managers/viewport-layout.manager.ts +10 -0
  50. package/src/app/stores/viewport-layout.store.ts +39 -3
  51. package/src/app/styles/design-system.css +388 -26
  52. package/src/app/test/vitest-setup.ts +31 -0
  53. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  54. package/src/features/agents/components/agents-page.tsx +10 -6
  55. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +3 -3
  56. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  57. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +4 -2
  58. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  59. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  60. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  61. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +15 -16
  62. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -6
  63. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +19 -19
  64. package/src/features/chat/components/conversation/chat-conversation-skeleton.tsx +13 -13
  65. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  66. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  67. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +18 -16
  68. package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
  69. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  70. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +93 -20
  71. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +93 -36
  72. package/src/features/chat/components/layout/chat-sidebar.tsx +60 -93
  73. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  74. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +225 -0
  75. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +199 -0
  76. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +308 -0
  77. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +439 -0
  78. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +194 -0
  79. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  80. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +51 -20
  81. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  82. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  83. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  84. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  85. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  86. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  87. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  88. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  89. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  90. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  91. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  92. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  93. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  94. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  95. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  96. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  97. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  98. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  99. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +7 -7
  100. package/src/features/chat/features/message/components/chat-message-list.container.tsx +99 -18
  101. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  102. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  103. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  104. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +7 -7
  105. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +6 -6
  106. package/src/features/chat/features/session/components/session-context-icon.tsx +2 -2
  107. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +2 -2
  108. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +1 -1
  109. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
  110. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  111. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  112. package/src/features/chat/features/session/components/session-run-badge.tsx +1 -1
  113. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  114. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  115. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  116. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  117. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  118. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  119. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  120. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +10 -2
  121. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  122. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -9
  123. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  124. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +3 -3
  125. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +4 -4
  126. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +14 -19
  127. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +5 -5
  128. package/src/features/chat/features/welcome/components/chat-welcome.tsx +6 -6
  129. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +6 -8
  130. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +1 -16
  131. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  132. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  133. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  134. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  135. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  136. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  137. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  138. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  139. package/src/features/chat/index.ts +0 -1
  140. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  141. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  142. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  143. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  144. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  145. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  146. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  147. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  148. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  149. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  150. package/src/features/panel-apps/components/panel-app-list-item.tsx +8 -8
  151. package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
  152. package/src/features/panel-apps/components/panel-apps-list.tsx +20 -20
  153. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  154. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +70 -6
  155. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +10 -5
  156. package/src/features/service-apps/components/service-action-authorization-dialog.tsx +8 -8
  157. package/src/features/service-apps/components/service-apps-panel.tsx +24 -24
  158. package/src/features/settings/components/config/provider-advanced-settings-section.tsx +5 -5
  159. package/src/features/settings/components/config/provider-auth-section.tsx +7 -7
  160. package/src/features/settings/components/config/provider-form.tsx +10 -10
  161. package/src/features/settings/components/config/provider-models-section.tsx +16 -16
  162. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  163. package/src/features/settings/pages/providers-config-page.tsx +17 -17
  164. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  165. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  166. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  167. package/src/shared/components/config-split-page.tsx +11 -11
  168. package/src/shared/components/doc-browser/doc-browser-home-page.tsx +2 -2
  169. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +4 -4
  170. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +4 -4
  171. package/src/shared/components/doc-browser/doc-browser.tsx +3 -3
  172. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +1 -1
  173. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  174. package/src/shared/components/ui/button.tsx +4 -4
  175. package/src/shared/components/ui/card.tsx +4 -4
  176. package/src/shared/components/ui/dialog.tsx +5 -5
  177. package/src/shared/components/ui/input.tsx +1 -1
  178. package/src/shared/components/ui/label.tsx +1 -1
  179. package/src/shared/components/ui/popover.tsx +1 -1
  180. package/src/shared/components/ui/select.tsx +3 -3
  181. package/src/shared/components/ui/switch.tsx +3 -3
  182. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  183. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  184. package/src/shared/components/ui/tabs.tsx +4 -4
  185. package/src/shared/components/ui/textarea.tsx +1 -1
  186. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  187. package/src/shared/lib/i18n/locales/en-US/core.json +9 -2
  188. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  189. package/src/shared/lib/i18n/locales/zh-CN/core.json +9 -2
  190. package/src/shared/lib/theme/index.ts +94 -52
  191. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  192. package/src/shared/lib/ui-document-title/index.ts +50 -21
  193. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  194. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  195. package/dist/assets/config-split-page-BCJhqdK9.js +0 -1
  196. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  197. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  198. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  199. package/dist/assets/index-7o1WdUbU.js +0 -103
  200. package/dist/assets/index-DQWmEWzk.css +0 -1
  201. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  202. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  203. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  204. package/dist/assets/plus-Bnt56oqG.js +0 -1
  205. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  206. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  207. package/dist/assets/remote-B0LMs8u7.js +0 -1
  208. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  209. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  210. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  211. package/dist/assets/x-CW2KO-Hz.js +0 -1
  212. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  213. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  214. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  215. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  216. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  217. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  218. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  219. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  220. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  221. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  222. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  223. package/src/features/chat/stores/chat-input.store.ts +0 -104
  224. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -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
  });
@@ -6,39 +6,18 @@ import {
6
6
  isNcpChatModelOptionsLoading,
7
7
  isNcpChatSendDisabled,
8
8
  } from '@/features/chat/features/input/utils/ncp-chat-input-availability.utils';
9
- import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
10
9
 
11
- function createSnapshot(
12
- overrides: Partial<ChatInputSnapshot> = {}
13
- ): ChatInputSnapshot {
10
+ type AvailabilitySnapshot = {
11
+ readonly isProviderStateResolved: boolean;
12
+ readonly modelOptions: readonly unknown[];
13
+ readonly sessionTypeUnavailable: boolean;
14
+ };
15
+
16
+ function createSnapshot(overrides: Partial<AvailabilitySnapshot> = {}): AvailabilitySnapshot {
14
17
  return {
15
18
  isProviderStateResolved: false,
16
- composerNodes: [],
17
- attachments: [],
18
- draft: '',
19
- pendingSessionType: 'native',
20
- pendingProjectRoot: null,
21
- pendingProjectRootSessionKey: null,
22
- defaultProjectRoot: null,
23
- defaultSessionType: 'native',
24
- canStopGeneration: false,
25
- stopDisabledReason: null,
26
- sendError: null,
27
- isSending: false,
28
19
  modelOptions: [],
29
- selectedModel: '',
30
- selectedThinkingLevel: null,
31
- sessionTypeOptions: [],
32
- selectedSessionType: 'native',
33
- stopSupported: false,
34
- stopReason: undefined,
35
- canEditSessionType: true,
36
20
  sessionTypeUnavailable: false,
37
- sessionTypeUnavailableMessage: null,
38
- skillRecords: [],
39
- isSkillsLoading: false,
40
- selectedSkills: [],
41
- composerFocusRequest: null,
42
21
  ...overrides,
43
22
  };
44
23
  }
@@ -12,6 +12,7 @@ import type { NcpMessagePart } from '@nextclaw/ncp';
12
12
  import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
13
13
 
14
14
  const CHAT_SKILL_TOKEN_PREFIX = '$';
15
+ const CHAT_PANEL_APP_TOKEN_PREFIX = '@panel-app:';
15
16
 
16
17
  function appendTextPart(parts: NcpMessagePart[], text: string): NcpMessagePart[] {
17
18
  if (text.length === 0) {
@@ -114,6 +115,11 @@ export function deriveNcpMessagePartsFromComposer(
114
115
  continue;
115
116
  }
116
117
 
118
+ if (node.tokenKind === 'panel_app') {
119
+ parts = appendTextPart(parts, `${CHAT_PANEL_APP_TOKEN_PREFIX}${node.tokenKey}`);
120
+ continue;
121
+ }
122
+
117
123
  if (node.tokenKind !== 'file') {
118
124
  continue;
119
125
  }
@@ -2,13 +2,11 @@ import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
2
2
 
3
3
  export const CHAT_UI_INLINE_TOKENS_METADATA_KEY = 'ui_inline_tokens';
4
4
  const CHAT_SKILL_TOKEN_PREFIX = '$';
5
+ const CHAT_PANEL_APP_TOKEN_PREFIX = '@panel-app:';
6
+ const CHAT_PANEL_APP_TOKEN_PATTERN = /@panel-app:([A-Za-z0-9_-]+)/g;
5
7
 
6
8
  export type ChatInlineTokenSource = { kind: string; key: string; label: string; rawText: string };
7
9
 
8
- export type ChatInlineTextFragment =
9
- | { type: 'text'; text: string }
10
- | { type: 'token'; token: ChatInlineTokenSource };
11
-
12
10
  function isRecord(value: unknown): value is Record<string, unknown> {
13
11
  return typeof value === 'object' && value !== null && !Array.isArray(value);
14
12
  }
@@ -51,6 +49,30 @@ export function buildInlineSkillTokensFromComposer(nodes: readonly ChatComposerN
51
49
  return dedupeInlineTokens(tokens);
52
50
  }
53
51
 
52
+ export function buildInlineTokensFromTextProtocol(text: string): ChatInlineTokenSource[] {
53
+ const tokens: ChatInlineTokenSource[] = [];
54
+ for (const match of text.matchAll(CHAT_PANEL_APP_TOKEN_PATTERN)) {
55
+ const key = match[1];
56
+ if (!key) {
57
+ continue;
58
+ }
59
+ tokens.push({
60
+ kind: 'panel_app',
61
+ key,
62
+ label: key,
63
+ rawText: `${CHAT_PANEL_APP_TOKEN_PREFIX}${key}`
64
+ });
65
+ }
66
+ return dedupeInlineTokens(tokens);
67
+ }
68
+
69
+ export function resolveInlineTokensForText(
70
+ text: string,
71
+ tokens: readonly ChatInlineTokenSource[]
72
+ ): ChatInlineTokenSource[] {
73
+ return dedupeInlineTokens([...tokens, ...buildInlineTokensFromTextProtocol(text)]);
74
+ }
75
+
54
76
  export function readInlineTokensFromMetadata(
55
77
  metadata: Record<string, unknown> | undefined
56
78
  ): ChatInlineTokenSource[] {
@@ -80,50 +102,3 @@ export function readInlineTokensFromMetadata(
80
102
 
81
103
  return dedupeInlineTokens(tokens);
82
104
  }
83
-
84
- export function splitTextByInlineTokens(
85
- text: string,
86
- tokens: readonly ChatInlineTokenSource[]
87
- ): ChatInlineTextFragment[] {
88
- if (text.length === 0 || tokens.length === 0) {
89
- return text.length === 0 ? [] : [{ type: 'text', text }];
90
- }
91
-
92
- const orderedTokens = [...tokens].sort((left, right) => right.rawText.length - left.rawText.length);
93
- const fragments: ChatInlineTextFragment[] = [];
94
- let cursor = 0;
95
-
96
- while (cursor < text.length) {
97
- let matchedToken: ChatInlineTokenSource | null = null;
98
- for (const token of orderedTokens) {
99
- if (text.startsWith(token.rawText, cursor)) {
100
- matchedToken = token;
101
- break;
102
- }
103
- }
104
-
105
- if (!matchedToken) {
106
- let nextCursor = cursor + 1;
107
- while (nextCursor < text.length) {
108
- if (orderedTokens.some((token) => text.startsWith(token.rawText, nextCursor))) {
109
- break;
110
- }
111
- nextCursor += 1;
112
- }
113
- fragments.push({
114
- type: 'text',
115
- text: text.slice(cursor, nextCursor)
116
- });
117
- cursor = nextCursor;
118
- continue;
119
- }
120
-
121
- fragments.push({
122
- type: 'token',
123
- token: matchedToken
124
- });
125
- cursor += matchedToken.rawText.length;
126
- }
127
-
128
- return fragments;
129
- }
@@ -178,6 +178,8 @@ export function buildChatSlashItems(
178
178
  `${texts.slashSkillSpecLabel}: ${record.key}`,
179
179
  ...(record.scopeLabel ? [`${texts.slashSkillScopeLabel}: ${record.scopeLabel}`] : [])
180
180
  ],
181
+ tokenKind: 'skill',
182
+ tokenKey: record.key,
181
183
  value: record.key
182
184
  }));
183
185
  }
@@ -1,9 +1,8 @@
1
- import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
2
-
3
- type ChatInputAvailabilitySnapshot = Pick<
4
- ChatInputSnapshot,
5
- 'isProviderStateResolved' | 'modelOptions' | 'sessionTypeUnavailable'
6
- >;
1
+ type ChatInputAvailabilitySnapshot = {
2
+ readonly isProviderStateResolved: boolean;
3
+ readonly modelOptions: readonly unknown[];
4
+ readonly sessionTypeUnavailable?: boolean;
5
+ };
7
6
 
8
7
  export function hasNcpChatModelOptions(
9
8
  snapshot: ChatInputAvailabilitySnapshot
@@ -1,4 +1,4 @@
1
- import { render } from "@testing-library/react";
1
+ import { render, screen } from "@testing-library/react";
2
2
  import type { NcpMessage } from "@nextclaw/ncp";
3
3
  import { beforeEach, expect, it, vi } from "vitest";
4
4
  import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
@@ -129,7 +129,7 @@ it("keeps historical adapted message references stable when only the streaming m
129
129
  expect(secondMessages[1]).not.toBe(firstMessages[1]);
130
130
  });
131
131
 
132
- it("adapts persisted inline token metadata into rich message parts", () => {
132
+ it("adapts persisted inline token metadata into markdown token data", () => {
133
133
  const message = {
134
134
  id: "user-inline-token",
135
135
  sessionId: "session-1",
@@ -156,24 +156,59 @@ it("adapts persisted inline token metadata into rich message parts", () => {
156
156
  expect(renderedMessages[0]).toMatchObject({
157
157
  parts: [
158
158
  {
159
- type: "inline-content",
160
- segments: [
161
- { type: "markdown", text: "please use " },
159
+ type: "markdown",
160
+ text: "please use $weather now",
161
+ inlineTokens: [
162
162
  {
163
- type: "token",
164
- token: {
165
- kind: "skill",
166
- key: "weather",
167
- label: "Weather",
168
- },
163
+ kind: "skill",
164
+ key: "weather",
165
+ label: "Weather",
166
+ rawText: "$weather",
169
167
  },
170
- { type: "markdown", text: " now" },
171
168
  ],
172
169
  },
173
170
  ],
174
171
  });
175
172
  });
176
173
 
174
+ it("renders context inheritance as a divider without repeating inherited messages", () => {
175
+ const inheritedMessage = {
176
+ id: "child-session:inherited:1",
177
+ sessionId: "child-session",
178
+ role: "user",
179
+ status: "final",
180
+ timestamp: "2026-03-31T10:00:00.000Z",
181
+ metadata: {
182
+ inherited_from_session_id: "parent-session",
183
+ inherited_from_message_id: "parent-message-1",
184
+ },
185
+ parts: [{ type: "text", text: "parent context" }],
186
+ } satisfies NcpMessage;
187
+ const childMessage = {
188
+ id: "child-message-1",
189
+ sessionId: "child-session",
190
+ role: "user",
191
+ status: "final",
192
+ timestamp: "2026-03-31T10:00:01.000Z",
193
+ parts: [{ type: "text", text: "child visible request" }],
194
+ } satisfies NcpMessage;
195
+
196
+ const { container } = render(
197
+ <ChatMessageListContainer
198
+ messages={[inheritedMessage, childMessage]}
199
+ isSending={false}
200
+ />,
201
+ );
202
+ const renderedMessages = captures.renders.flatMap((rendered) => rendered.messages);
203
+ const renderedPayload = JSON.stringify(renderedMessages);
204
+
205
+ expect(screen.getByText("chatContextInheritanceInherited")).toBeTruthy();
206
+ expect(container.querySelector('[title*="parent-session"]')).toBeTruthy();
207
+ expect(container.querySelector('[title*="chatContextInheritanceMessages: 1"]')).toBeTruthy();
208
+ expect(renderedPayload).toContain("child visible request");
209
+ expect(renderedPayload).not.toContain("parent context");
210
+ });
211
+
177
212
  it("keeps Hermes tool invocation parts as tool cards instead of flattening them into plain text", () => {
178
213
  const message = {
179
214
  id: "assistant-hermes-tool-1",
@@ -77,28 +77,28 @@ export function ChatInlinePanelAppCard({
77
77
  if (!url) {
78
78
  return (
79
79
  <div
80
- className="h-[320px] min-h-[220px] w-full animate-pulse rounded-lg border border-[#eee3d1] bg-gradient-to-b from-white to-amber-50/60"
80
+ className="h-[320px] min-h-[220px] w-full animate-pulse rounded-lg border border-border bg-muted/45"
81
81
  style={{ maxHeight: PANEL_APP_INLINE_CARD_MAX_HEIGHT_PX }}
82
82
  />
83
83
  );
84
84
  }
85
85
 
86
86
  return (
87
- <div className="w-full max-w-[42rem] overflow-hidden rounded-lg border border-[#eee3d1] bg-white shadow-[0_1px_2px_rgba(30,20,10,0.04)]">
88
- <div className="flex h-9 items-center justify-between gap-2 border-b border-[#f1e7d4] bg-[#fffaf2] px-2.5">
89
- <div className="flex min-w-0 items-center gap-2 text-xs font-medium text-[#5c4933]">
87
+ <div className="w-full max-w-[42rem] overflow-hidden rounded-lg border border-border bg-card shadow-sm">
88
+ <div className="flex h-9 items-center justify-between gap-2 border-b border-border bg-muted/45 px-2.5">
89
+ <div className="flex min-w-0 items-center gap-2 text-xs font-medium text-foreground">
90
90
  <AppWindow className="h-3.5 w-3.5 shrink-0 text-primary" />
91
91
  <span className="truncate">{title}</span>
92
92
  </div>
93
93
  <IconActionButton
94
- className="h-7 w-7 rounded-md text-[#7a6145] hover:bg-white hover:text-[#2f2212]"
94
+ className="h-7 w-7 rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
95
95
  icon={<Maximize2 className="h-3.5 w-3.5" />}
96
96
  label={t('chatPanelCardExpand')}
97
97
  onClick={openExpanded}
98
98
  />
99
99
  </div>
100
100
  <div
101
- className="relative h-[420px] min-h-[220px] max-h-[min(60vh,420px)] overflow-hidden bg-white"
101
+ className="relative h-[420px] min-h-[220px] max-h-[min(60vh,420px)] overflow-hidden bg-card"
102
102
  >
103
103
  <iframe
104
104
  ref={iframeRef}
@@ -107,7 +107,7 @@ export function ChatInlinePanelAppCard({
107
107
  title={title}
108
108
  sandbox={PANEL_APP_INLINE_IFRAME_SANDBOX}
109
109
  scrolling="auto"
110
- className="h-full w-full border-0 bg-white"
110
+ className="h-full w-full border-0 bg-card"
111
111
  />
112
112
  </div>
113
113
  </div>
@@ -27,11 +27,22 @@ type ChatMessageListContainerProps = {
27
27
  className?: string;
28
28
  };
29
29
 
30
+ const INHERITED_FROM_SESSION_METADATA_KEY = "inherited_from_session_id";
31
+
30
32
  const messageViewModelCache = new WeakMap<
31
33
  NcpMessage,
32
34
  { language: string; viewModel: ChatMessageViewModel }
33
35
  >();
34
36
 
37
+ type ContextInheritanceTimelineView = {
38
+ sourceSessionId: string;
39
+ inheritedMessageCount: number;
40
+ };
41
+
42
+ type ContextInheritanceTimelineBoundary = ContextInheritanceTimelineView & {
43
+ boundaryIndex: number;
44
+ };
45
+
35
46
  type ChatTimelineItem =
36
47
  | {
37
48
  kind: "messages";
@@ -42,6 +53,11 @@ type ChatTimelineItem =
42
53
  kind: "compaction";
43
54
  key: string;
44
55
  checkpoint: ContextCompactionTimelineView;
56
+ }
57
+ | {
58
+ kind: "context-inheritance";
59
+ key: string;
60
+ inheritance: ContextInheritanceTimelineView;
45
61
  };
46
62
 
47
63
  function buildChatMessageAdapterTexts(
@@ -110,13 +126,13 @@ function ChatContextCompactionDivider({
110
126
  `${t("chatContextCompactionProjectedTokens")}: ${checkpoint.projectedEstimatedTokens}`,
111
127
  ].join("\n");
112
128
  return (
113
- <div className="my-4 flex items-center gap-3 text-[11px] text-gray-500" title={title}>
114
- <div className="h-px flex-1 bg-gray-200" />
115
- <div className="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-gray-50 px-3 py-1">
129
+ <div className="my-4 flex items-center gap-3 text-[11px] text-muted-foreground" title={title}>
130
+ <div className="h-px flex-1 bg-border" />
131
+ <div className="inline-flex items-center gap-2 rounded-full border border-border bg-muted px-3 py-1">
116
132
  {checkpoint.status === "compressing" ? (
117
- <span className="h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse" />
133
+ <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-muted-foreground" />
118
134
  ) : (
119
- <span className="h-1.5 w-1.5 rounded-full bg-gray-300" />
135
+ <span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/45" />
120
136
  )}
121
137
  <span>
122
138
  {checkpoint.status === "compressing"
@@ -124,40 +140,86 @@ function ChatContextCompactionDivider({
124
140
  : t("chatContextCompactionCompressed")}
125
141
  </span>
126
142
  </div>
127
- <div className="h-px flex-1 bg-gray-200" />
143
+ <div className="h-px flex-1 bg-border" />
144
+ </div>
145
+ );
146
+ }
147
+
148
+ function ChatContextInheritanceDivider({
149
+ inheritance,
150
+ }: {
151
+ inheritance: ContextInheritanceTimelineView;
152
+ }) {
153
+ const title = [
154
+ `${t("chatContextInheritanceSourceSession")}: ${inheritance.sourceSessionId}`,
155
+ `${t("chatContextInheritanceMessages")}: ${inheritance.inheritedMessageCount}`,
156
+ ].join("\n");
157
+ return (
158
+ <div className="my-4 flex items-center gap-3 text-[11px] text-emerald-700" title={title}>
159
+ <div className="h-px flex-1 bg-emerald-100" />
160
+ <div className="inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1">
161
+ <span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
162
+ <span>{t("chatContextInheritanceInherited")}</span>
163
+ </div>
164
+ <div className="h-px flex-1 bg-emerald-100" />
128
165
  </div>
129
166
  );
130
167
  }
131
168
 
132
169
  function resolveCompactionBoundaryIndex(params: {
133
170
  rawMessages: readonly NcpMessage[];
134
- normalRawMessages: readonly NcpMessage[];
171
+ visibleRawMessages: readonly NcpMessage[];
135
172
  rawMessageId: string;
136
173
  }): number {
137
174
  const {
138
- normalRawMessages,
139
175
  rawMessageId,
140
176
  rawMessages,
177
+ visibleRawMessages,
141
178
  } = params;
142
179
  const physicalIndex = rawMessages.findIndex(
143
180
  (message) => message.id === rawMessageId,
144
181
  );
145
182
  if (physicalIndex < 0) {
146
- return normalRawMessages.length - 1;
183
+ return visibleRawMessages.length - 1;
147
184
  }
148
- const previousNormalCount = rawMessages
185
+ const previousVisibleCount = rawMessages
149
186
  .slice(0, physicalIndex)
150
- .filter((message) => !readContextCompactionTimeline(message)).length;
151
- return previousNormalCount - 1;
187
+ .filter(isVisibleChatMessage).length;
188
+ return previousVisibleCount - 1;
189
+ }
190
+
191
+ function readInheritedSourceSessionId(message: NcpMessage): string | null {
192
+ const value = message.metadata?.[INHERITED_FROM_SESSION_METADATA_KEY];
193
+ return typeof value === "string" && value.trim() ? value.trim() : null;
194
+ }
195
+
196
+ function isVisibleChatMessage(message: NcpMessage): boolean {
197
+ return !readContextCompactionTimeline(message) && !readInheritedSourceSessionId(message);
198
+ }
199
+
200
+ function resolveContextInheritanceBoundary(
201
+ messages: readonly NcpMessage[],
202
+ ): ContextInheritanceTimelineBoundary | null {
203
+ const boundaryIndex = messages.findIndex((message) => readInheritedSourceSessionId(message));
204
+ if (boundaryIndex < 0) {
205
+ return null;
206
+ }
207
+ const sourceSessionId = readInheritedSourceSessionId(messages[boundaryIndex]);
208
+ if (!sourceSessionId) {
209
+ return null;
210
+ }
211
+ return {
212
+ boundaryIndex: messages.slice(0, boundaryIndex).filter(isVisibleChatMessage).length,
213
+ sourceSessionId,
214
+ inheritedMessageCount: messages.filter((message) => readInheritedSourceSessionId(message) === sourceSessionId).length,
215
+ };
152
216
  }
153
217
 
154
218
  function buildTimelineItems(params: {
155
219
  rawMessages: readonly NcpMessage[];
156
220
  messages: ChatMessageViewModel[];
157
221
  }): ChatTimelineItem[] {
158
- const normalRawMessages = params.rawMessages.filter(
159
- (message) => !readContextCompactionTimeline(message),
160
- );
222
+ const visibleRawMessages = params.rawMessages.filter(isVisibleChatMessage);
161
223
  const checkpoints = params.rawMessages
162
224
  .map((message) => ({
163
225
  rawMessageId: message.id,
@@ -172,11 +234,12 @@ function buildTimelineItems(params: {
172
234
  checkpoint: entry.checkpoint,
173
235
  boundaryIndex: resolveCompactionBoundaryIndex({
174
236
  rawMessages: params.rawMessages,
175
- normalRawMessages,
237
+ visibleRawMessages,
176
238
  rawMessageId: entry.rawMessageId,
177
239
  }),
178
240
  }))
179
241
  .sort((left, right) => left.boundaryIndex - right.boundaryIndex);
242
+ const contextInheritance = resolveContextInheritanceBoundary(params.rawMessages);
180
243
 
181
244
  const items: ChatTimelineItem[] = [];
182
245
  let pendingMessages: ChatMessageViewModel[] = [];
@@ -193,7 +256,15 @@ function buildTimelineItems(params: {
193
256
  pendingMessages = [];
194
257
  };
195
258
 
196
- normalRawMessages.forEach((rawMessage, index) => {
259
+ visibleRawMessages.forEach((rawMessage, index) => {
260
+ if (contextInheritance?.boundaryIndex === index) {
261
+ flushPendingMessages("messages-before-context-inheritance");
262
+ items.push({
263
+ kind: "context-inheritance",
264
+ key: "context-inheritance",
265
+ inheritance: contextInheritance,
266
+ });
267
+ }
197
268
  const message = params.messages[index];
198
269
  if (message) {
199
270
  pendingMessages.push(message);
@@ -209,6 +280,14 @@ function buildTimelineItems(params: {
209
280
  checkpointCursor += 1;
210
281
  }
211
282
  });
283
+ if (contextInheritance?.boundaryIndex === visibleRawMessages.length) {
284
+ flushPendingMessages("messages-before-context-inheritance");
285
+ items.push({
286
+ kind: "context-inheritance",
287
+ key: "context-inheritance",
288
+ inheritance: contextInheritance,
289
+ });
290
+ }
212
291
  while (checkpointCursor < checkpoints.length) {
213
292
  const currentCheckpoint = checkpoints[checkpointCursor];
214
293
  flushPendingMessages(`messages-before-${currentCheckpoint.key}`);
@@ -244,7 +323,7 @@ export function ChatMessageListContainer({
244
323
 
245
324
  const messages = useMemo(() => {
246
325
  return rawMessages.flatMap((message) => {
247
- if (readContextCompactionTimeline(message)) {
326
+ if (!isVisibleChatMessage(message)) {
248
327
  return [];
249
328
  }
250
329
  const cached = messageViewModelCache.get(message);
@@ -295,6 +374,8 @@ export function ChatMessageListContainer({
295
374
  {timelineItems.map((item, index) =>
296
375
  item.kind === "compaction" ? (
297
376
  <ChatContextCompactionDivider key={item.key} checkpoint={item.checkpoint} />
377
+ ) : item.kind === "context-inheritance" ? (
378
+ <ChatContextInheritanceDivider key={item.key} inheritance={item.inheritance} />
298
379
  ) : (
299
380
  <ChatMessageList
300
381
  key={item.key}