@nextclaw/ui 0.14.0 → 0.14.1-beta.1

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 +41 -0
  2. package/dist/assets/{api-B50yVdT8.js → api-dQ6uQSZG.js} +3 -3
  3. package/dist/assets/channels-list-page-DfpjB8XB.js +8 -0
  4. package/dist/assets/chat-page-D3QaEqjs.js +105 -0
  5. package/dist/assets/config-split-page-CeFfbaqc.js +1 -0
  6. package/dist/assets/{confirm-dialog-DRSelLdI.js → confirm-dialog-C95Y4QPC.js} +1 -1
  7. package/dist/assets/desktop-update-config-CcK6jiEb.js +1 -0
  8. package/dist/assets/{dist-D19wiQwB.js → dist-DTGzeil6.js} +1 -1
  9. package/dist/assets/{dist-NZRV-BxD.js → dist-fGogBmPA.js} +1 -1
  10. package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
  11. package/dist/assets/doc-browser-DVHhEb96.js +1 -0
  12. package/dist/assets/{doc-browser-context-C8KPbj-p.js → doc-browser-context-BfMXwnrz.js} +1 -1
  13. package/dist/assets/{ellipsis-BYy8TP0A.js → ellipsis-D2F4KMiU.js} +1 -1
  14. package/dist/assets/{external-link-528k6MDD.js → external-link-CcgtBK26.js} +1 -1
  15. package/dist/assets/index-ByJCbQqr.css +1 -0
  16. package/dist/assets/index-DjXqiloY.js +103 -0
  17. package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
  19. package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
  20. package/dist/assets/plus-1Rxl5FlV.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
  22. package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
  23. package/dist/assets/{react-Dsotw5vM.js → react-CTqdhFMk.js} +1 -1
  24. package/dist/assets/remote-fQ7N_r8u.js +1 -0
  25. package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
  26. package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
  27. package/dist/assets/{save-CHDnDTok.js → save-C9fKWfVB.js} +1 -1
  28. package/dist/assets/search-D5FQ5q0X.js +1 -0
  29. package/dist/assets/search-config-page-AMEJJNIS.js +1 -0
  30. package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
  31. package/dist/assets/select-Cu5n0E-3.js +41 -0
  32. package/dist/assets/{tag-chip-Cr8GwpRE.js → tag-chip-C5aOtjxu.js} +1 -1
  33. package/dist/assets/use-config-D8TESeo6.js +1 -0
  34. package/dist/assets/x-1qETdQ70.js +1 -0
  35. package/dist/index.html +14 -27
  36. package/package.json +9 -9
  37. package/src/app/presenters/app.presenter.ts +2 -0
  38. package/src/features/account/components/account-panel.tsx +2 -2
  39. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
  40. package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
  41. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -0
  42. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
  43. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +122 -85
  44. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +120 -0
  45. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
  46. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +66 -41
  47. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
  48. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +22 -36
  49. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -52
  50. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +89 -0
  51. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +9 -1
  52. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +115 -0
  53. package/src/features/chat/features/message/components/chat-message-list.container.tsx +4 -0
  54. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
  55. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +119 -1
  56. package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
  57. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
  58. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +51 -0
  59. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +28 -5
  60. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +14 -9
  61. package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
  62. package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
  63. package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
  64. package/src/features/chat/features/ncp/hooks/__tests__/use-ui-show-content-event.test.tsx +1 -0
  65. package/src/features/chat/features/ncp/utils/ncp-chat-query-derived.utils.ts +11 -3
  66. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +27 -0
  67. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
  68. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +19 -1
  69. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +46 -0
  70. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
  71. package/src/features/chat/features/session/hooks/__tests__/use-selected-session-context-window-indicator.test.tsx +63 -0
  72. package/src/features/chat/features/session/hooks/use-selected-session-context-window-indicator.ts +7 -10
  73. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +8 -0
  74. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -0
  75. package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
  76. package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
  77. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +1 -0
  78. package/src/features/chat/features/session-type/utils/chat-session-type.utils.ts +4 -0
  79. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
  80. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
  81. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
  82. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
  83. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
  84. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
  85. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
  86. package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
  87. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
  88. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
  89. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
  90. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
  91. package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
  92. package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
  93. package/src/features/chat/index.ts +1 -0
  94. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +93 -0
  95. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +129 -0
  96. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +34 -0
  97. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +143 -1
  98. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +30 -0
  99. package/src/features/chat/managers/__tests__/recent-selection.manager.test.ts +37 -14
  100. package/src/features/chat/managers/chat-draft-intent.manager.ts +46 -0
  101. package/src/features/chat/managers/chat-input.manager.ts +37 -15
  102. package/src/features/chat/managers/chat-run.manager.ts +3 -0
  103. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +19 -2
  104. package/src/features/chat/managers/chat-thread.manager.ts +4 -0
  105. package/src/features/chat/managers/chat-ui.manager.ts +2 -6
  106. package/src/features/chat/managers/recent-selection.manager.ts +21 -67
  107. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +47 -1
  108. package/src/features/chat/pages/ncp-chat-page.tsx +18 -0
  109. package/src/features/chat/stores/chat-input.store.ts +9 -1
  110. package/src/features/chat/stores/recent-selection.store.ts +85 -0
  111. package/src/features/chat/types/chat-input-bar.types.ts +5 -1
  112. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
  113. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +137 -0
  114. package/src/features/panel-apps/components/panel-apps-list.tsx +119 -5
  115. package/src/features/panel-apps/index.ts +2 -0
  116. package/src/features/panel-apps/utils/panel-app-entry-match.utils.ts +17 -0
  117. package/src/features/settings/components/config/provider-form.tsx +284 -387
  118. package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
  119. package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
  120. package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
  121. package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
  122. package/src/features/settings/pages/secrets-config-page.tsx +2 -1
  123. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
  124. package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
  125. package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
  126. package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
  127. package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
  128. package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
  129. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
  130. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
  131. package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
  132. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
  133. package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
  134. package/src/features/system-status/components/runtime-security-card.tsx +79 -25
  135. package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
  136. package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
  137. package/src/shared/components/common/searchable-model-input.tsx +102 -87
  138. package/src/shared/components/ui/README.md +7 -0
  139. package/src/shared/components/ui/popover.tsx +19 -3
  140. package/src/shared/components/ui/select.tsx +14 -4
  141. package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
  142. package/src/shared/lib/api/chat-session-type.types.ts +7 -0
  143. package/src/shared/lib/api/index.ts +2 -0
  144. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  145. package/src/shared/lib/api/preferences/preference.types.ts +22 -0
  146. package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
  147. package/src/shared/lib/api/types.ts +3 -0
  148. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -1
  149. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  150. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
  151. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -1
  152. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  153. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
  154. package/dist/assets/app-presenter-provider-BI-ewLbO.js +0 -3
  155. package/dist/assets/arrow-left-BOCJyLbQ.js +0 -1
  156. package/dist/assets/book-open-Bd2Xbrrc.js +0 -1
  157. package/dist/assets/channels-list-page-moJkpxtU.js +0 -8
  158. package/dist/assets/chat-page-CO27cl4v.js +0 -105
  159. package/dist/assets/config-split-page-CsQ2rIdq.js +0 -1
  160. package/dist/assets/desktop-update-config-qa-SynOc.js +0 -1
  161. package/dist/assets/doc-browser-2ddUJCA0.js +0 -1
  162. package/dist/assets/doc-browser-BjHUczQb.js +0 -1
  163. package/dist/assets/es2015-BUIIKCNh.js +0 -41
  164. package/dist/assets/host-capabilities-9bB_998Z.js +0 -1
  165. package/dist/assets/index-CMNrRhnG.js +0 -101
  166. package/dist/assets/index-UTmTW1eU.css +0 -1
  167. package/dist/assets/key-round-D3vD9dj7.js +0 -1
  168. package/dist/assets/loader-circle-DV9TSRns.js +0 -1
  169. package/dist/assets/mcp-marketplace-page-CS5Lh3oA.js +0 -1
  170. package/dist/assets/mcp-marketplace-page-DEOMcWb0.js +0 -40
  171. package/dist/assets/model-config-page-BSc4o7pb.js +0 -1
  172. package/dist/assets/notice-card-qqlbsOoR.js +0 -1
  173. package/dist/assets/plus-Bf9qhPDO.js +0 -1
  174. package/dist/assets/popover-BnmPOXe3.js +0 -1
  175. package/dist/assets/provider-scoped-model-input-Ca56s475.js +0 -1
  176. package/dist/assets/providers-config-page-D8Kf4U3u.js +0 -1
  177. package/dist/assets/refresh-cw-Bmx4wc9P.js +0 -1
  178. package/dist/assets/remote-DNMEy_c4.js +0 -1
  179. package/dist/assets/rotate-cw-CL0sVBel.js +0 -1
  180. package/dist/assets/runtime-config-page-CUFtKr0U.js +0 -1
  181. package/dist/assets/search-D7L-TCH-.js +0 -1
  182. package/dist/assets/search-config-page-CA3RVAef.js +0 -1
  183. package/dist/assets/secrets-config-page-95yaH1fa.js +0 -3
  184. package/dist/assets/status-dot-Dm4h-bjn.js +0 -1
  185. package/dist/assets/tabs-CJvw5Lqp.js +0 -1
  186. package/dist/assets/tooltip-Bxzi6aDL.js +0 -1
  187. package/dist/assets/trash-2-CSfX-BvU.js +0 -1
  188. package/dist/assets/use-config-TzwzTgg0.js +0 -1
  189. package/dist/assets/x-fkIWvi07.js +0 -1
  190. package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
  191. package/src/features/chat/components/chat-welcome.tsx +0 -102
  192. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-bar-controller.test.tsx +0 -152
  193. package/src/features/chat/features/input/hooks/use-chat-input-bar-controller.ts +0 -142
  194. /package/dist/assets/{config-hints-Cl2XAhzV.js → config-hints-CTqBnCDp.js} +0 -0
@@ -0,0 +1,46 @@
1
+ import {
2
+ Dialog,
3
+ DialogContent,
4
+ DialogDescription,
5
+ DialogHeader,
6
+ DialogTitle,
7
+ } from '@/shared/components/ui/dialog';
8
+ import { t } from '@/shared/lib/i18n';
9
+
10
+ type ChatSessionMetadataDialogProps = {
11
+ open: boolean;
12
+ metadata?: Record<string, unknown> | null;
13
+ onOpenChange: (open: boolean) => void;
14
+ };
15
+
16
+ function formatSessionMetadata(metadata?: Record<string, unknown> | null): string {
17
+ try {
18
+ return JSON.stringify(metadata ?? {}, null, 2);
19
+ } catch {
20
+ return '{}';
21
+ }
22
+ }
23
+
24
+ export function ChatSessionMetadataDialog({
25
+ open,
26
+ metadata,
27
+ onOpenChange,
28
+ }: ChatSessionMetadataDialogProps) {
29
+ return (
30
+ <Dialog open={open} onOpenChange={onOpenChange}>
31
+ <DialogContent className="max-w-2xl gap-0 overflow-hidden p-0">
32
+ <DialogHeader className="border-b border-gray-200 px-5 py-4 pr-12">
33
+ <DialogTitle className="text-base">{t('chatSessionMetadataDialogTitle')}</DialogTitle>
34
+ <DialogDescription className="sr-only">
35
+ {t('chatSessionMetadataDialogDescription')}
36
+ </DialogDescription>
37
+ </DialogHeader>
38
+ <div className="px-5 pb-5 pt-4">
39
+ <pre className="max-h-[60vh] overflow-auto rounded-lg border border-gray-200 bg-gray-950 p-4 text-xs leading-relaxed text-gray-100">
40
+ {formatSessionMetadata(metadata)}
41
+ </pre>
42
+ </div>
43
+ </DialogContent>
44
+ </Dialog>
45
+ );
46
+ }
@@ -3,7 +3,12 @@ import { ChevronDown, FolderOpen, FolderX, Pencil } from 'lucide-react';
3
3
  import { useChatSessionProject } from '@/features/chat/features/session/hooks/use-chat-session-project';
4
4
  import { ChatSessionHeaderMenuItem } from './chat-session-header-menu-item';
5
5
  import { ChatSessionProjectDialog } from './chat-session-project-dialog';
6
- import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
6
+ import {
7
+ Popover,
8
+ PopoverContent,
9
+ PopoverTrigger,
10
+ createPopoverAvailableHeightLimit,
11
+ } from '@/shared/components/ui/popover';
7
12
  import { t } from '@/shared/lib/i18n';
8
13
 
9
14
  type ChatSessionProjectBadgeProps = {
@@ -13,6 +18,10 @@ type ChatSessionProjectBadgeProps = {
13
18
  persistToServer: boolean;
14
19
  };
15
20
 
21
+ const SESSION_PROJECT_MENU_STYLE = {
22
+ maxHeight: createPopoverAvailableHeightLimit('18rem'),
23
+ };
24
+
16
25
  export function ChatSessionProjectBadge({
17
26
  sessionKey,
18
27
  projectName,
@@ -57,7 +66,11 @@ export function ChatSessionProjectBadge({
57
66
  </span>
58
67
  </button>
59
68
  </PopoverTrigger>
60
- <PopoverContent align="start" className="w-72 p-2">
69
+ <PopoverContent
70
+ align="start"
71
+ className="w-72 p-2"
72
+ style={SESSION_PROJECT_MENU_STYLE}
73
+ >
61
74
  <div className="px-3 pb-2 pt-1">
62
75
  <div className="text-[11px] font-medium uppercase tracking-wider text-gray-500">
63
76
  {projectName}
@@ -0,0 +1,63 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import { useSelectedSessionContextWindowIndicator } from '@/features/chat/features/session/hooks/use-selected-session-context-window-indicator';
4
+ import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
5
+ import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
6
+
7
+ describe('useSelectedSessionContextWindowIndicator', () => {
8
+ beforeEach(() => {
9
+ useChatSessionListStore.getState().setSnapshot({
10
+ selectedSessionKey: null,
11
+ });
12
+ useChatThreadStore.getState().setSnapshot({
13
+ sessionKey: null,
14
+ contextWindow: null,
15
+ });
16
+ });
17
+
18
+ it('shows the current thread context window once the thread snapshot has it', () => {
19
+ useChatSessionListStore.getState().setSnapshot({
20
+ selectedSessionKey: 'session-other',
21
+ });
22
+ useChatThreadStore.getState().setSnapshot({
23
+ sessionKey: 'session-current',
24
+ contextWindow: {
25
+ usedContextTokens: 25,
26
+ totalContextTokens: 100,
27
+ availableContextTokens: 75,
28
+ prunedUsedContextTokens: 25,
29
+ droppedHistoryCount: 0,
30
+ truncatedToolResultCount: 0,
31
+ truncatedSystemPrompt: false,
32
+ truncatedUserMessage: false,
33
+ compacted: false,
34
+ compactedUsedContextTokens: 25,
35
+ compactedMessageCount: 0,
36
+ updatedAt: '2026-06-10T00:00:00.000Z',
37
+ },
38
+ });
39
+
40
+ const { result } = renderHook(() =>
41
+ useSelectedSessionContextWindowIndicator(),
42
+ );
43
+
44
+ expect(result.current).toMatchObject({
45
+ percentLabel: '25%',
46
+ ratio: 0.25,
47
+ tone: 'neutral',
48
+ });
49
+ });
50
+
51
+ it('hides the indicator when the current thread snapshot has no context window', () => {
52
+ useChatThreadStore.getState().setSnapshot({
53
+ sessionKey: 'session-current',
54
+ contextWindow: null,
55
+ });
56
+
57
+ const { result } = renderHook(() =>
58
+ useSelectedSessionContextWindowIndicator(),
59
+ );
60
+
61
+ expect(result.current).toBeNull();
62
+ });
63
+ });
@@ -1,18 +1,15 @@
1
1
  import { useMemo } from 'react';
2
2
  import type { ChatContextWindowIndicator } from '@nextclaw/agent-chat-ui';
3
- import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
4
3
  import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
5
4
  import { buildChatContextWindowIndicator } from '@/features/chat/features/session/utils/chat-context-window-indicator.utils';
6
5
 
7
6
  export function useSelectedSessionContextWindowIndicator(): ChatContextWindowIndicator | null {
8
- const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
9
- const liveSessionKey = useChatThreadStore((state) => state.snapshot.sessionKey);
10
- const liveContextWindow = useChatThreadStore((state) => state.snapshot.contextWindow);
7
+ const liveContextWindow = useChatThreadStore(
8
+ (state) => state.snapshot.contextWindow,
9
+ );
11
10
 
12
- return useMemo(() => {
13
- if (selectedSessionKey && liveSessionKey === selectedSessionKey && liveContextWindow) {
14
- return buildChatContextWindowIndicator(liveContextWindow);
15
- }
16
- return null;
17
- }, [liveContextWindow, liveSessionKey, selectedSessionKey]);
11
+ return useMemo(
12
+ () => buildChatContextWindowIndicator(liveContextWindow),
13
+ [liveContextWindow],
14
+ );
18
15
  }
@@ -43,6 +43,14 @@ describe('adaptNcpSessionSummary', () => {
43
43
  projectRoot: '/Users/demo/workspace/project-alpha',
44
44
  workingDir: '/Users/demo/workspace/project-alpha',
45
45
  projectName: 'project-alpha',
46
+ metadata: {
47
+ label: 'NCP Planning Thread',
48
+ model: 'openai/gpt-5',
49
+ preferred_thinking: 'medium',
50
+ project_root: '/Users/demo/workspace/project-alpha',
51
+ session_type: 'native',
52
+ ui_last_read_at: '2026-03-17T23:59:00.000Z'
53
+ },
46
54
  lastMessageAt: '2026-03-18T00:00:00.000Z',
47
55
  readAt: '2026-03-17T23:59:00.000Z',
48
56
  sessionType: 'native',
@@ -308,6 +308,7 @@ export function adaptNcpMessagesToUiMessages(messages: readonly NcpMessageView[]
308
308
  }
309
309
 
310
310
  export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionEntryView {
311
+ const metadata = readMetadata(summary);
311
312
  const label = readNcpSessionLabel(summary);
312
313
  const preferredModel = readNcpSessionPreferredModel(summary);
313
314
  const preferredThinking = readNcpSessionPreferredThinking(summary);
@@ -335,6 +336,7 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
335
336
  ...(projectRoot ? { projectRoot } : {}),
336
337
  ...(workingDir ? { workingDir } : {}),
337
338
  ...(projectName ? { projectName } : {}),
339
+ ...(metadata ? { metadata } : {}),
338
340
  sessionType: readNcpSessionType(summary),
339
341
  sessionTypeMutable: false,
340
342
  status: summary.status ?? 'idle',
@@ -0,0 +1,24 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ChatSidebarCreateMenu } from "@/features/chat/features/session-type/components/chat-sidebar-create-menu";
4
+ import { createPopoverAvailableHeightLimit } from "@/shared/components/ui/popover";
5
+
6
+ const options = [
7
+ { value: "native", label: "Native", ready: true },
8
+ { value: "codex", label: "Codex", ready: true },
9
+ ];
10
+
11
+ describe("ChatSidebarCreateMenu", () => {
12
+ it("keeps the session type menu height bounded", () => {
13
+ render(<ChatSidebarCreateMenu options={options} onSelect={vi.fn()} />);
14
+
15
+ const createMenu = screen.getByText("Session Type").parentElement;
16
+ expect(createMenu?.style.maxHeight).toBe(
17
+ createPopoverAvailableHeightLimit("18rem"),
18
+ );
19
+ expect(createMenu?.style.maxHeight).toContain("max(0px");
20
+ expect(createMenu?.style.maxHeight).toContain("100vh");
21
+ expect(createMenu?.style.maxHeight).toContain("2rem");
22
+ expect(createMenu?.className).toContain("overflow-y-auto");
23
+ });
24
+ });
@@ -1,9 +1,15 @@
1
1
  import { ChatSessionTypeOptionItem } from "@/features/chat/features/session-type/components/chat-session-type-option-item";
2
2
  import type { ChatInputSnapshot } from "@/features/chat/stores/chat-input.store";
3
+ import { createPopoverAvailableHeightLimit } from "@/shared/components/ui/popover";
3
4
  import { t } from "@/shared/lib/i18n";
4
5
  import { cn } from "@/shared/lib/utils";
5
6
 
6
7
  type SessionTypeOption = ChatInputSnapshot["sessionTypeOptions"][number];
8
+ const CHAT_SIDEBAR_CREATE_MENU_MAX_HEIGHT =
9
+ createPopoverAvailableHeightLimit("18rem");
10
+ const CHAT_SIDEBAR_CREATE_MENU_STYLE = {
11
+ maxHeight: CHAT_SIDEBAR_CREATE_MENU_MAX_HEIGHT,
12
+ };
7
13
 
8
14
  type ChatSidebarCreateMenuProps = {
9
15
  options: readonly SessionTypeOption[];
@@ -21,7 +27,10 @@ export function ChatSidebarCreateMenu({
21
27
  titleClassName,
22
28
  }: ChatSidebarCreateMenuProps) {
23
29
  return (
24
- <div className={cn("space-y-1", className)}>
30
+ <div
31
+ className={cn("space-y-1 overflow-y-auto overscroll-contain", className)}
32
+ style={CHAT_SIDEBAR_CREATE_MENU_STYLE}
33
+ >
25
34
  <div
26
35
  className={cn(
27
36
  "px-3 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-400",
@@ -55,6 +55,7 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
55
55
  supportedModels: undefined,
56
56
  recommendedModel: null,
57
57
  modelSelectionMode: 'nextclaw',
58
+ runtimeDefaultThinking: null,
58
59
  cta: null
59
60
  });
60
61
  }
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AgentProfileView,
3
3
  ChatSessionTypeOptionView,
4
+ RuntimeDefaultThinkingView,
4
5
  SessionTypeIconView,
5
6
  } from "@/shared/lib/api";
6
7
  import type { RuntimeModelSelectionMode } from "@nextclaw/shared";
@@ -18,6 +19,7 @@ export type ChatSessionTypeOption = {
18
19
  supportedModels?: string[];
19
20
  recommendedModel?: string | null;
20
21
  modelSelectionMode?: RuntimeModelSelectionMode;
22
+ runtimeDefaultThinking?: RuntimeDefaultThinkingView | null;
21
23
  cta?: {
22
24
  kind: string;
23
25
  label?: string;
@@ -76,6 +78,7 @@ export function buildSessionTypeOptions(
76
78
  supportedModels: option.supportedModels,
77
79
  recommendedModel: option.recommendedModel ?? null,
78
80
  modelSelectionMode: option.modelSelectionMode ?? "nextclaw",
81
+ runtimeDefaultThinking: option.runtimeDefaultThinking ?? null,
79
82
  cta: option.cta ?? null,
80
83
  });
81
84
  }
@@ -90,6 +93,7 @@ export function buildSessionTypeOptions(
90
93
  supportedModels: undefined,
91
94
  recommendedModel: null,
92
95
  modelSelectionMode: "nextclaw",
96
+ runtimeDefaultThinking: null,
93
97
  cta: null,
94
98
  });
95
99
  }
@@ -0,0 +1,193 @@
1
+ import type { ReactNode } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { ChatConversationWelcome } from "@/features/chat/features/welcome/components/chat-conversation-welcome";
6
+ import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
7
+ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
8
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
9
+ import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
10
+ import type { ChatQuerySnapshot } from "@/features/chat/stores/ncp-chat-query.store";
11
+
12
+ const mocks = vi.hoisted(() => ({
13
+ setSelectedAgentId: vi.fn(),
14
+ applyPromptSuggestion: vi.fn(),
15
+ setPendingProjectRoot: vi.fn(),
16
+ setPendingSessionType: vi.fn(),
17
+ agents: [
18
+ { id: "main", displayName: "Main", runtime: "native" },
19
+ { id: "engineer", displayName: "Engineer", runtime: "codex" },
20
+ ],
21
+ }));
22
+
23
+ vi.mock("@/features/chat/features/welcome/components/chat-welcome", () => ({
24
+ ChatWelcome: ({
25
+ inputSlot,
26
+ onSelectAgent,
27
+ onSelectPrompt,
28
+ onSelectProjectRoot,
29
+ onSelectSessionType,
30
+ }: {
31
+ inputSlot: ReactNode;
32
+ onSelectAgent: (agentId: string) => void;
33
+ onSelectPrompt: (prompt: string) => void;
34
+ onSelectProjectRoot: (projectRoot: string) => void;
35
+ onSelectSessionType: (sessionType: string) => void;
36
+ }) => (
37
+ <div>
38
+ {inputSlot}
39
+ <button type="button" onClick={() => onSelectAgent("engineer")}>
40
+ switch draft agent
41
+ </button>
42
+ <button type="button" onClick={() => onSelectPrompt("example prompt")}>
43
+ pick prompt
44
+ </button>
45
+ <button type="button" onClick={() => onSelectSessionType("codex")}>
46
+ switch session type
47
+ </button>
48
+ <button
49
+ type="button"
50
+ onClick={() => onSelectProjectRoot("/tmp/project-alpha")}
51
+ >
52
+ select draft project
53
+ </button>
54
+ </div>
55
+ ),
56
+ }));
57
+
58
+ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
59
+ usePresenter: () => ({
60
+ chatSessionListManager: {
61
+ setSelectedAgentId: mocks.setSelectedAgentId,
62
+ },
63
+ chatInputManager: {
64
+ applyPromptSuggestion: mocks.applyPromptSuggestion,
65
+ setPendingProjectRoot: mocks.setPendingProjectRoot,
66
+ setPendingSessionType: mocks.setPendingSessionType,
67
+ },
68
+ }),
69
+ }));
70
+
71
+ vi.mock("@/shared/hooks/use-agents", () => ({
72
+ useAgents: () => ({
73
+ data: { agents: mocks.agents },
74
+ }),
75
+ }));
76
+
77
+ function createFetchedQuery<TData>(data: TData) {
78
+ return {
79
+ data,
80
+ error: null,
81
+ fetchStatus: "idle",
82
+ isFetched: true,
83
+ isFetching: false,
84
+ isLoading: false,
85
+ isSuccess: true,
86
+ status: "success",
87
+ };
88
+ }
89
+
90
+ function resetWelcomeTestState() {
91
+ mocks.setSelectedAgentId.mockReset();
92
+ mocks.applyPromptSuggestion.mockReset();
93
+ mocks.setPendingProjectRoot.mockReset();
94
+ mocks.setPendingSessionType.mockReset();
95
+ useChatInputStore.setState({
96
+ snapshot: {
97
+ ...useChatInputStore.getState().snapshot,
98
+ defaultSessionType: "native",
99
+ pendingSessionType: "native",
100
+ selectedSessionType: undefined,
101
+ pendingProjectRoot: null,
102
+ },
103
+ });
104
+ useChatSessionListStore.setState({
105
+ snapshot: {
106
+ ...useChatSessionListStore.getState().snapshot,
107
+ selectedAgentId: "main",
108
+ },
109
+ });
110
+ useChatThreadStore.setState({
111
+ snapshot: {
112
+ ...useChatThreadStore.getState().snapshot,
113
+ agentId: null,
114
+ },
115
+ });
116
+ useChatQueryStore.setState({
117
+ snapshot: {
118
+ configQuery: createFetchedQuery({
119
+ agents: {
120
+ defaults: {
121
+ workspace: "/Users/demo/.nextclaw/workspace",
122
+ },
123
+ },
124
+ providers: {},
125
+ }) as unknown as ChatQuerySnapshot["configQuery"],
126
+ sessionTypesQuery: createFetchedQuery({
127
+ defaultType: "native",
128
+ options: [
129
+ { value: "native", label: "Native", ready: true },
130
+ { value: "codex", label: "Codex", ready: true },
131
+ ],
132
+ }) as unknown as ChatQuerySnapshot["sessionTypesQuery"],
133
+ sessionsQuery: createFetchedQuery({
134
+ sessions: [],
135
+ total: 0,
136
+ }) as unknown as ChatQuerySnapshot["sessionsQuery"],
137
+ },
138
+ });
139
+ }
140
+
141
+ describe("ChatConversationWelcome", () => {
142
+ beforeEach(resetWelcomeTestState);
143
+
144
+ it("stores the selected welcome project through the input manager", async () => {
145
+ const user = userEvent.setup();
146
+
147
+ render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
148
+
149
+ await user.click(
150
+ screen.getByRole("button", { name: "select draft project" }),
151
+ );
152
+
153
+ expect(mocks.setPendingProjectRoot).toHaveBeenCalledWith("/tmp/project-alpha");
154
+ });
155
+
156
+ it("applies a selected prompt suggestion through the input manager", async () => {
157
+ const user = userEvent.setup();
158
+
159
+ render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
160
+
161
+ await user.click(
162
+ screen.getByRole("button", { name: "pick prompt" }),
163
+ );
164
+
165
+ expect(mocks.applyPromptSuggestion).toHaveBeenCalledWith("example prompt");
166
+ });
167
+
168
+
169
+ it("syncs the pending session type when switching the draft agent", async () => {
170
+ const user = userEvent.setup();
171
+
172
+ render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
173
+
174
+ await user.click(
175
+ screen.getByRole("button", { name: "switch draft agent" }),
176
+ );
177
+
178
+ expect(mocks.setSelectedAgentId).toHaveBeenCalledWith("engineer");
179
+ expect(mocks.setPendingSessionType).toHaveBeenCalledWith("codex");
180
+ });
181
+
182
+ it("stores an explicit welcome session type selection", async () => {
183
+ const user = userEvent.setup();
184
+
185
+ render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
186
+
187
+ await user.click(
188
+ screen.getByRole("button", { name: "switch session type" }),
189
+ );
190
+
191
+ expect(mocks.setPendingSessionType).toHaveBeenCalledWith("codex");
192
+ });
193
+ });
@@ -0,0 +1,123 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { ChatWelcome } from '@/features/chat/features/welcome/components/chat-welcome';
4
+
5
+ vi.mock(
6
+ '@/features/chat/features/session/components/session-header/chat-session-project-dialog',
7
+ () => ({
8
+ ChatSessionProjectDialog: () => null,
9
+ }),
10
+ );
11
+
12
+ const sessionTypeOptions = [
13
+ { value: 'native', label: 'Native', ready: true },
14
+ { value: 'codex', label: 'Codex', ready: true },
15
+ ];
16
+
17
+ function renderWelcome(
18
+ overrides: Partial<Parameters<typeof ChatWelcome>[0]> = {},
19
+ ) {
20
+ return render(
21
+ <ChatWelcome
22
+ agents={[
23
+ { id: 'main', displayName: 'Main' },
24
+ { id: 'engineer', displayName: 'Engineer' },
25
+ ]}
26
+ projectOptions={[]}
27
+ selectedAgentId="main"
28
+ selectedSessionType="native"
29
+ sessionTypeOptions={sessionTypeOptions}
30
+ onSelectAgent={vi.fn()}
31
+ onSelectPrompt={vi.fn()}
32
+ onSelectSessionType={vi.fn()}
33
+ {...overrides}
34
+ />,
35
+ );
36
+ }
37
+
38
+ describe('ChatWelcome', () => {
39
+ it('renders a lightweight draft agent select and allows switching', () => {
40
+ const onSelectAgent = vi.fn();
41
+
42
+ renderWelcome({ onSelectAgent });
43
+
44
+ const trigger = screen.getByRole('combobox', { name: 'Draft agent' });
45
+ expect(screen.getByText('Main')).toBeTruthy();
46
+ fireEvent.keyDown(trigger, { key: 'ArrowDown' });
47
+ fireEvent.click(screen.getByText('Engineer'));
48
+
49
+ expect(onSelectAgent).toHaveBeenCalledWith('engineer');
50
+ expect(screen.queryByText('Current Agent:')).toBeNull();
51
+ });
52
+
53
+ it('uses an adaptive capability grid for narrow docked layouts', () => {
54
+ renderWelcome({ agents: [{ id: 'main', displayName: 'Main' }] });
55
+
56
+ expect(
57
+ screen.getByRole('button', { name: /Smart Conversations/ }).parentElement
58
+ ?.className,
59
+ ).toContain('auto-fit');
60
+ });
61
+
62
+ it('fills the composer with a prompt suggestion when clicking a capability card', () => {
63
+ const onSelectPrompt = vi.fn();
64
+
65
+ renderWelcome({ onSelectPrompt });
66
+
67
+ fireEvent.click(screen.getByRole('button', { name: /Smart Conversations/ }));
68
+
69
+ expect(onSelectPrompt).toHaveBeenCalledWith(
70
+ expect.stringContaining('next three concrete things'),
71
+ );
72
+ });
73
+
74
+ it('lets users switch the welcome session type', () => {
75
+ const onSelectSessionType = vi.fn();
76
+
77
+ renderWelcome({ onSelectSessionType });
78
+
79
+ fireEvent.click(screen.getByRole('button', { name: 'Choose session type' }));
80
+ fireEvent.click(screen.getByText('Codex'));
81
+
82
+ expect(onSelectSessionType).toHaveBeenCalledWith('codex');
83
+ });
84
+
85
+ it('shows recent projects in a scrollable menu with a fixed folder action', async () => {
86
+ const onSelectProjectRoot = vi.fn();
87
+
88
+ renderWelcome({
89
+ defaultProjectRoot: '/Users/demo/.nextclaw/workspace',
90
+ onSelectProjectRoot,
91
+ projectOptions: [
92
+ {
93
+ projectRoot: '/Users/demo/.nextclaw/workspace',
94
+ projectName: 'workspace',
95
+ sessionCount: 0,
96
+ isDefault: true,
97
+ },
98
+ {
99
+ projectRoot: '/tmp/project-alpha',
100
+ projectName: 'project-alpha',
101
+ sessionCount: 3,
102
+ isDefault: false,
103
+ },
104
+ ],
105
+ });
106
+
107
+ fireEvent.click(
108
+ screen.getByRole('button', { name: 'Choose working directory' }),
109
+ );
110
+ expect(screen.getByText('Recent projects')).toBeTruthy();
111
+ expect(screen.getByText('/tmp/project-alpha')).toBeTruthy();
112
+ expect(screen.getByText('Open folder')).toBeTruthy();
113
+ expect(screen.getByText('/tmp/project-alpha').closest('.overflow-y-auto'))
114
+ .toBeTruthy();
115
+
116
+ fireEvent.click(screen.getByText('project-alpha'));
117
+
118
+ expect(onSelectProjectRoot).toHaveBeenCalledWith('/tmp/project-alpha');
119
+ await waitFor(() => {
120
+ expect(screen.queryByText('Recent projects')).toBeNull();
121
+ });
122
+ });
123
+ });