@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,104 @@
1
+ import type { ReactNode } from "react";
2
+ import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
3
+ import {
4
+ buildSessionTypeOptions,
5
+ DEFAULT_SESSION_TYPE,
6
+ normalizeSessionType,
7
+ } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
8
+ import { ChatWelcome } from "@/features/chat/features/welcome/components/chat-welcome";
9
+ import {
10
+ resolveChatWelcomeAgents,
11
+ resolveChatWelcomeSelectedSessionType,
12
+ } from "@/features/chat/features/welcome/utils/chat-welcome-selection.utils";
13
+ import { buildChatWelcomeProjectOptions } from "@/features/chat/features/welcome/utils/chat-welcome-project-options.utils";
14
+ import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
15
+ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
16
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
17
+ import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
18
+ import { useAgents } from "@/shared/hooks/use-agents";
19
+ import type { NcpSessionSummaryView } from "@/shared/lib/api";
20
+ import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
21
+
22
+ const EMPTY_NCP_SESSION_SUMMARIES: NcpSessionSummaryView[] = [];
23
+
24
+ type ChatConversationWelcomeProps = {
25
+ inputSlot: ReactNode;
26
+ };
27
+
28
+ export function ChatConversationWelcome({
29
+ inputSlot,
30
+ }: ChatConversationWelcomeProps) {
31
+ const presenter = usePresenter();
32
+ const inputSnapshot = useChatInputStore((state) => state.snapshot);
33
+ const selectedAgentId = useChatSessionListStore(
34
+ (state) => state.snapshot.selectedAgentId,
35
+ );
36
+ const threadSnapshot = useChatThreadStore((state) => state.snapshot);
37
+ const config = useChatQueryStore(
38
+ (state) => state.snapshot.configQuery?.data ?? null,
39
+ );
40
+ const sessionSummaries = useChatQueryStore(
41
+ (state) => state.snapshot.sessionsQuery?.data?.sessions ?? EMPTY_NCP_SESSION_SUMMARIES,
42
+ );
43
+ const sessionTypesData = useChatQueryStore(
44
+ (state) => state.snapshot.sessionTypesQuery?.data ?? null,
45
+ );
46
+ const agentsQuery = useAgents();
47
+ const draftAgentId = threadSnapshot.agentId ?? selectedAgentId;
48
+ const availableAgents = resolveChatWelcomeAgents({
49
+ agents: agentsQuery.data?.agents,
50
+ fallbackAgentId: draftAgentId,
51
+ });
52
+ const defaultProjectRoot = normalizeSessionProjectRootValue(
53
+ config?.agents.defaults.workspace,
54
+ );
55
+ const selectedProjectRoot = normalizeSessionProjectRootValue(
56
+ inputSnapshot.pendingProjectRoot,
57
+ );
58
+ const sessionTypeOptions = buildSessionTypeOptions(
59
+ sessionTypesData?.options ?? [],
60
+ );
61
+ const defaultSessionType = normalizeSessionType(
62
+ sessionTypesData?.defaultType ?? inputSnapshot.defaultSessionType ?? DEFAULT_SESSION_TYPE,
63
+ );
64
+ const selectedSessionType = resolveChatWelcomeSelectedSessionType({
65
+ defaultSessionType,
66
+ pendingSessionType: inputSnapshot.pendingSessionType,
67
+ selectedSessionType: inputSnapshot.selectedSessionType,
68
+ sessionTypeOptions,
69
+ });
70
+ const projectOptions = buildChatWelcomeProjectOptions({
71
+ defaultProjectRoot,
72
+ sessionSummaries,
73
+ });
74
+ const selectDraftAgent = (agentId: string) => {
75
+ presenter.chatSessionListManager.setSelectedAgentId(agentId);
76
+ presenter.chatInputManager.setPendingSessionType(
77
+ resolveChatWelcomeSelectedSessionType({
78
+ agents: availableAgents,
79
+ agentId,
80
+ defaultSessionType,
81
+ pendingSessionType: null,
82
+ selectedSessionType: null,
83
+ sessionTypeOptions,
84
+ }),
85
+ );
86
+ };
87
+
88
+ return (
89
+ <ChatWelcome
90
+ agents={availableAgents}
91
+ defaultProjectRoot={defaultProjectRoot}
92
+ inputSlot={inputSlot}
93
+ projectOptions={projectOptions}
94
+ selectedAgentId={draftAgentId}
95
+ selectedProjectRoot={selectedProjectRoot}
96
+ selectedSessionType={selectedSessionType}
97
+ sessionTypeOptions={sessionTypeOptions}
98
+ onSelectAgent={selectDraftAgent}
99
+ onSelectPrompt={presenter.chatInputManager.applyPromptSuggestion}
100
+ onSelectProjectRoot={presenter.chatInputManager.setPendingProjectRoot}
101
+ onSelectSessionType={presenter.chatInputManager.setPendingSessionType}
102
+ />
103
+ );
104
+ }
@@ -0,0 +1,72 @@
1
+ import { AgentAvatar } from '@/shared/components/common/agent-avatar';
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectTrigger,
7
+ createSelectAvailableHeightLimit,
8
+ } from '@/shared/components/ui/select';
9
+ import type { AgentProfileView } from '@/shared/lib/api';
10
+ import { t } from '@/shared/lib/i18n';
11
+
12
+ type ChatWelcomeAgentPickerProps = {
13
+ agents: AgentProfileView[];
14
+ selectedAgent: AgentProfileView | null;
15
+ selectedAgentId: string;
16
+ onSelectAgent: (agentId: string) => void;
17
+ };
18
+
19
+ const AGENT_PICKER_MAX_HEIGHT = createSelectAvailableHeightLimit('18rem');
20
+
21
+ export function ChatWelcomeAgentPicker({
22
+ agents,
23
+ selectedAgent,
24
+ selectedAgentId,
25
+ onSelectAgent,
26
+ }: ChatWelcomeAgentPickerProps) {
27
+ return (
28
+ <Select value={selectedAgentId} onValueChange={onSelectAgent}>
29
+ <SelectTrigger
30
+ aria-label={t('chatDraftAgentTitle')}
31
+ className="h-auto w-auto min-w-0 gap-1 rounded-lg border-0 bg-transparent px-2 py-1.5 text-gray-500 shadow-none hover:bg-gray-100 hover:text-gray-800 focus:ring-0"
32
+ >
33
+ <span className="sr-only">{t('chatDraftAgentTitle')}</span>
34
+ <div className="flex min-w-0 items-center gap-1.5">
35
+ {selectedAgent ? (
36
+ <>
37
+ <AgentAvatar
38
+ agentId={selectedAgent.id}
39
+ displayName={selectedAgent.displayName}
40
+ avatarUrl={selectedAgent.avatarUrl}
41
+ className="h-5 w-5 shrink-0"
42
+ />
43
+ <span className="max-w-28 truncate text-sm font-medium">
44
+ {selectedAgent.displayName?.trim() || selectedAgent.id}
45
+ </span>
46
+ </>
47
+ ) : null}
48
+ </div>
49
+ </SelectTrigger>
50
+ <SelectContent
51
+ className="rounded-xl border-gray-200/80 shadow-lg"
52
+ style={{ maxHeight: AGENT_PICKER_MAX_HEIGHT }}
53
+ >
54
+ {agents.map((agent) => (
55
+ <SelectItem key={agent.id} value={agent.id} className="rounded-lg pr-10">
56
+ <div className="flex min-w-0 items-center gap-2">
57
+ <AgentAvatar
58
+ agentId={agent.id}
59
+ displayName={agent.displayName}
60
+ avatarUrl={agent.avatarUrl}
61
+ className="h-5 w-5 shrink-0"
62
+ />
63
+ <span className="truncate text-sm font-medium text-gray-700">
64
+ {agent.displayName?.trim() || agent.id}
65
+ </span>
66
+ </div>
67
+ </SelectItem>
68
+ ))}
69
+ </SelectContent>
70
+ </Select>
71
+ );
72
+ }
@@ -0,0 +1,54 @@
1
+ import { AlarmClock, BrainCircuit, MessageCircle } from 'lucide-react';
2
+ import { t } from '@/shared/lib/i18n';
3
+
4
+ const WELCOME_CAPABILITIES = [
5
+ {
6
+ icon: MessageCircle,
7
+ titleKey: 'chatWelcomeCapability1Title' as const,
8
+ descKey: 'chatWelcomeCapability1Desc' as const,
9
+ promptKey: 'chatWelcomeCapability1Prompt' as const,
10
+ },
11
+ {
12
+ icon: BrainCircuit,
13
+ titleKey: 'chatWelcomeCapability2Title' as const,
14
+ descKey: 'chatWelcomeCapability2Desc' as const,
15
+ promptKey: 'chatWelcomeCapability2Prompt' as const,
16
+ },
17
+ {
18
+ icon: AlarmClock,
19
+ titleKey: 'chatWelcomeCapability3Title' as const,
20
+ descKey: 'chatWelcomeCapability3Desc' as const,
21
+ promptKey: 'chatWelcomeCapability3Prompt' as const,
22
+ },
23
+ ];
24
+
25
+ export function ChatWelcomeCapabilityGrid({
26
+ onSelectPrompt,
27
+ }: {
28
+ onSelectPrompt: (prompt: string) => void;
29
+ }) {
30
+ return (
31
+ <div className="mt-8 grid grid-cols-[repeat(auto-fit,minmax(min(100%,9.5rem),1fr))] gap-3">
32
+ {WELCOME_CAPABILITIES.map((capability) => {
33
+ const Icon = capability.icon;
34
+ return (
35
+ <button
36
+ key={capability.titleKey}
37
+ onClick={() => onSelectPrompt(t(capability.promptKey))}
38
+ className="min-w-0 rounded-2xl border border-gray-200 bg-white p-3 text-left shadow-card transition-shadow hover:shadow-card-hover sm:p-4"
39
+ >
40
+ <div className="mb-3 flex h-9 w-9 items-center justify-center rounded-xl bg-primary/8">
41
+ <Icon className="h-4.5 w-4.5 text-primary" />
42
+ </div>
43
+ <div className="mb-1 text-sm font-semibold text-gray-900">
44
+ {t(capability.titleKey)}
45
+ </div>
46
+ <div className="text-[11px] leading-relaxed text-gray-500">
47
+ {t(capability.descKey)}
48
+ </div>
49
+ </button>
50
+ );
51
+ })}
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,132 @@
1
+ import { useState } from 'react';
2
+ import { ChevronDown, FolderOpen } from 'lucide-react';
3
+ import type { ChatWelcomeProjectOption } from '@/features/chat/features/welcome/utils/chat-welcome-project-options.utils';
4
+ import {
5
+ Popover,
6
+ PopoverContent,
7
+ PopoverTrigger,
8
+ createPopoverAvailableHeightLimit,
9
+ } from '@/shared/components/ui/popover';
10
+ import { t } from '@/shared/lib/i18n';
11
+ import { getSessionProjectName } from '@/shared/lib/session-project';
12
+
13
+ type ChatWelcomeProjectPickerProps = {
14
+ defaultProjectRoot?: string | null;
15
+ isSaving: boolean;
16
+ projectOptions: readonly ChatWelcomeProjectOption[];
17
+ projectRoot: string | null;
18
+ selectable: boolean;
19
+ onOpenProjectDialog: () => void;
20
+ onSelectProjectRoot: (projectRoot: string) => Promise<void> | void;
21
+ };
22
+
23
+ const PROJECT_PICKER_MAX_HEIGHT = createPopoverAvailableHeightLimit('20rem');
24
+
25
+ export function ChatWelcomeProjectPicker({
26
+ defaultProjectRoot,
27
+ isSaving,
28
+ projectOptions,
29
+ projectRoot,
30
+ selectable,
31
+ onOpenProjectDialog,
32
+ onSelectProjectRoot,
33
+ }: ChatWelcomeProjectPickerProps) {
34
+ const [isOpen, setIsOpen] = useState(false);
35
+ const projectLabel =
36
+ getSessionProjectName(projectRoot) ?? t('chatWelcomeProjectPickerPlaceholder');
37
+ const isDefaultProject =
38
+ defaultProjectRoot !== null &&
39
+ defaultProjectRoot !== undefined &&
40
+ projectRoot === defaultProjectRoot;
41
+
42
+ const selectProjectRoot = async (nextProjectRoot: string) => {
43
+ setIsOpen(false);
44
+ await onSelectProjectRoot(nextProjectRoot);
45
+ };
46
+
47
+ return (
48
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
49
+ <PopoverTrigger asChild>
50
+ <button
51
+ type="button"
52
+ className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
53
+ title={projectRoot ?? undefined}
54
+ aria-label={t('chatWelcomeProjectPickerLabel')}
55
+ disabled={!selectable || isSaving}
56
+ >
57
+ <FolderOpen className="h-4 w-4 shrink-0" />
58
+ <span className="truncate">{projectLabel}</span>
59
+ {isDefaultProject ? (
60
+ <span className="shrink-0 text-xs font-normal text-gray-400">
61
+ {t('chatWelcomeProjectDefaultBadge')}
62
+ </span>
63
+ ) : null}
64
+ <ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
65
+ </button>
66
+ </PopoverTrigger>
67
+ <PopoverContent
68
+ align="start"
69
+ className="flex w-[min(20rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-gray-200/80 bg-white p-0 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
70
+ style={{ maxHeight: PROJECT_PICKER_MAX_HEIGHT }}
71
+ >
72
+ <div className="shrink-0 px-3 pb-2 pt-3 text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-400">
73
+ {t('chatWelcomeProjectRecentTitle')}
74
+ </div>
75
+ <div className="min-h-0 flex-1 overflow-y-auto px-1.5 pb-1.5">
76
+ {projectOptions.length > 0 ? (
77
+ projectOptions.map((option) => (
78
+ <button
79
+ key={option.projectRoot}
80
+ type="button"
81
+ className="flex w-full min-w-0 items-start gap-2 rounded-xl px-2 py-2 text-left transition-colors hover:bg-gray-50"
82
+ title={option.projectRoot}
83
+ onClick={() => {
84
+ void selectProjectRoot(option.projectRoot);
85
+ }}
86
+ >
87
+ <FolderOpen className="mt-0.5 h-4 w-4 shrink-0 text-gray-400" />
88
+ <span className="min-w-0 flex-1">
89
+ <span className="flex min-w-0 items-center gap-1.5">
90
+ <span className="truncate text-[13px] font-semibold text-gray-800">
91
+ {option.projectName}
92
+ </span>
93
+ {option.isDefault ? (
94
+ <span className="shrink-0 text-[10px] font-medium text-gray-400">
95
+ {t('chatWelcomeProjectDefaultBadge')}
96
+ </span>
97
+ ) : null}
98
+ </span>
99
+ <span className="block truncate text-[11px] leading-4 text-gray-500">
100
+ {option.projectRoot}
101
+ </span>
102
+ </span>
103
+ {option.sessionCount > 0 ? (
104
+ <span className="mt-0.5 shrink-0 rounded-full bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500">
105
+ {option.sessionCount}
106
+ </span>
107
+ ) : null}
108
+ </button>
109
+ ))
110
+ ) : (
111
+ <div className="px-2 py-5 text-center text-xs text-gray-400">
112
+ {t('chatWelcomeProjectNoRecent')}
113
+ </div>
114
+ )}
115
+ </div>
116
+ <div className="shrink-0 border-t border-gray-100 p-1.5">
117
+ <button
118
+ type="button"
119
+ className="flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left text-[13px] font-semibold text-gray-700 transition-colors hover:bg-gray-50"
120
+ onClick={() => {
121
+ setIsOpen(false);
122
+ onOpenProjectDialog();
123
+ }}
124
+ >
125
+ <FolderOpen className="h-4 w-4 shrink-0 text-gray-400" />
126
+ <span>{t('chatWelcomeProjectOpenFolder')}</span>
127
+ </button>
128
+ </div>
129
+ </PopoverContent>
130
+ </Popover>
131
+ );
132
+ }
@@ -0,0 +1,82 @@
1
+ import { useState } from 'react';
2
+ import { Bot, ChevronDown } from 'lucide-react';
3
+ import { SessionContextIconNode } from '@/features/chat/features/session/components/session-context-icon';
4
+ import { ChatSessionTypeOptionItem } from '@/features/chat/features/session-type/components/chat-session-type-option-item';
5
+ import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
6
+ import {
7
+ Popover,
8
+ PopoverContent,
9
+ PopoverTrigger,
10
+ createPopoverAvailableHeightLimit,
11
+ } from '@/shared/components/ui/popover';
12
+ import { t } from '@/shared/lib/i18n';
13
+
14
+ type SessionTypeOption = ChatInputSnapshot['sessionTypeOptions'][number];
15
+
16
+ type ChatWelcomeSessionTypePickerProps = {
17
+ options: readonly SessionTypeOption[];
18
+ selectedSessionType: string;
19
+ onSelectSessionType: (sessionType: string) => void;
20
+ };
21
+
22
+ const SESSION_TYPE_PICKER_MAX_HEIGHT = createPopoverAvailableHeightLimit('18rem');
23
+
24
+ export function ChatWelcomeSessionTypePicker({
25
+ options,
26
+ selectedSessionType,
27
+ onSelectSessionType,
28
+ }: ChatWelcomeSessionTypePickerProps) {
29
+ const [isOpen, setIsOpen] = useState(false);
30
+ const selectedOption =
31
+ options.find((option) => option.value === selectedSessionType) ?? options[0] ?? null;
32
+
33
+ if (!selectedOption) {
34
+ return null;
35
+ }
36
+
37
+ return (
38
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
39
+ <PopoverTrigger asChild>
40
+ <button
41
+ type="button"
42
+ className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
43
+ aria-label={t('chatWelcomeSessionTypePickerLabel')}
44
+ >
45
+ {selectedOption.icon?.src ? (
46
+ <SessionContextIconNode
47
+ icon={{
48
+ kind: 'runtime-image',
49
+ src: selectedOption.icon.src,
50
+ alt: selectedOption.icon.alt ?? null,
51
+ name: selectedOption.label,
52
+ }}
53
+ className="h-4 w-4"
54
+ />
55
+ ) : (
56
+ <Bot className="h-4 w-4 shrink-0" />
57
+ )}
58
+ <span className="truncate">{selectedOption.label}</span>
59
+ <ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
60
+ </button>
61
+ </PopoverTrigger>
62
+ <PopoverContent
63
+ align="start"
64
+ className="flex w-[min(16rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
65
+ style={{ maxHeight: SESSION_TYPE_PICKER_MAX_HEIGHT }}
66
+ >
67
+ <div className="min-h-0 flex-1 overflow-y-auto">
68
+ {options.map((option) => (
69
+ <ChatSessionTypeOptionItem
70
+ key={option.value}
71
+ option={option}
72
+ onSelect={() => {
73
+ onSelectSessionType(option.value);
74
+ setIsOpen(false);
75
+ }}
76
+ />
77
+ ))}
78
+ </div>
79
+ </PopoverContent>
80
+ </Popover>
81
+ );
82
+ }
@@ -0,0 +1,121 @@
1
+ import type { ReactNode } from 'react';
2
+ import { useState } from 'react';
3
+ import { Bot } from 'lucide-react';
4
+ import { ChatSessionProjectDialog } from '@/features/chat/features/session/components/session-header/chat-session-project-dialog';
5
+ import { ChatWelcomeAgentPicker } from '@/features/chat/features/welcome/components/chat-welcome-agent-picker';
6
+ import { ChatWelcomeCapabilityGrid } from '@/features/chat/features/welcome/components/chat-welcome-capability-grid';
7
+ import { ChatWelcomeProjectPicker } from '@/features/chat/features/welcome/components/chat-welcome-project-picker';
8
+ import { ChatWelcomeSessionTypePicker } from '@/features/chat/features/welcome/components/chat-welcome-session-type-picker';
9
+ import type { ChatWelcomeProjectOption } from '@/features/chat/features/welcome/utils/chat-welcome-project-options.utils';
10
+ import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
11
+ import type { AgentProfileView } from '@/shared/lib/api';
12
+ import { t } from '@/shared/lib/i18n';
13
+
14
+ type SessionTypeOption = ChatInputSnapshot['sessionTypeOptions'][number];
15
+
16
+ type ChatWelcomeProps = {
17
+ agents: AgentProfileView[];
18
+ inputSlot?: ReactNode;
19
+ defaultProjectRoot?: string | null;
20
+ projectOptions: readonly ChatWelcomeProjectOption[];
21
+ selectedAgentId: string;
22
+ selectedProjectRoot?: string | null;
23
+ selectedSessionType: string;
24
+ sessionTypeOptions: readonly SessionTypeOption[];
25
+ onSelectAgent: (agentId: string) => void;
26
+ onSelectPrompt: (prompt: string) => void;
27
+ onSelectProjectRoot?: (projectRoot: string) => Promise<void> | void;
28
+ onSelectSessionType: (sessionType: string) => void;
29
+ };
30
+
31
+ export function ChatWelcome({
32
+ agents,
33
+ defaultProjectRoot,
34
+ inputSlot,
35
+ projectOptions,
36
+ selectedAgentId,
37
+ selectedProjectRoot,
38
+ selectedSessionType,
39
+ sessionTypeOptions,
40
+ onSelectAgent,
41
+ onSelectPrompt,
42
+ onSelectProjectRoot,
43
+ onSelectSessionType,
44
+ }: ChatWelcomeProps) {
45
+ const selectedAgent = agents.find((agent) => agent.id === selectedAgentId) ?? null;
46
+ const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false);
47
+ const [isProjectSaving, setIsProjectSaving] = useState(false);
48
+ const resolvedProjectRoot = selectedProjectRoot ?? defaultProjectRoot ?? null;
49
+
50
+ const saveProjectRoot = async (projectRoot: string) => {
51
+ if (!onSelectProjectRoot) {
52
+ return;
53
+ }
54
+ setIsProjectSaving(true);
55
+ try {
56
+ await onSelectProjectRoot(projectRoot);
57
+ setIsProjectDialogOpen(false);
58
+ } finally {
59
+ setIsProjectSaving(false);
60
+ }
61
+ };
62
+ const selectableProjectRoot = Boolean(onSelectProjectRoot);
63
+
64
+ return (
65
+ <div className="flex min-h-full items-center justify-center px-4 py-8 sm:p-8">
66
+ <div className="min-w-0 w-full max-w-[min(760px,100%)]">
67
+ <div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10">
68
+ <Bot className="h-8 w-8 text-primary" />
69
+ </div>
70
+
71
+ <div className="text-center">
72
+ <h2 className="text-2xl font-semibold text-gray-950 sm:text-3xl">
73
+ {t('chatWelcomeTitle')}
74
+ </h2>
75
+ <p className="mt-3 text-sm font-medium text-gray-500 sm:text-base">
76
+ {t('chatWelcomeSubtitle')}
77
+ </p>
78
+ </div>
79
+
80
+ {inputSlot ? <div className="mt-8">{inputSlot}</div> : null}
81
+
82
+ <div className="mt-3 flex flex-wrap items-center gap-2 px-1 text-sm text-gray-500">
83
+ {onSelectProjectRoot ? (
84
+ <ChatWelcomeProjectPicker
85
+ defaultProjectRoot={defaultProjectRoot}
86
+ isSaving={isProjectSaving}
87
+ projectOptions={projectOptions}
88
+ projectRoot={resolvedProjectRoot}
89
+ selectable={selectableProjectRoot}
90
+ onOpenProjectDialog={() => setIsProjectDialogOpen(true)}
91
+ onSelectProjectRoot={saveProjectRoot}
92
+ />
93
+ ) : null}
94
+ <ChatWelcomeAgentPicker
95
+ agents={agents}
96
+ selectedAgent={selectedAgent}
97
+ selectedAgentId={selectedAgentId}
98
+ onSelectAgent={onSelectAgent}
99
+ />
100
+ <ChatWelcomeSessionTypePicker
101
+ options={sessionTypeOptions}
102
+ selectedSessionType={selectedSessionType}
103
+ onSelectSessionType={onSelectSessionType}
104
+ />
105
+ </div>
106
+
107
+ <ChatWelcomeCapabilityGrid onSelectPrompt={onSelectPrompt} />
108
+ </div>
109
+
110
+ {onSelectProjectRoot ? (
111
+ <ChatSessionProjectDialog
112
+ open={isProjectDialogOpen}
113
+ currentProjectRoot={resolvedProjectRoot}
114
+ isSaving={isProjectSaving}
115
+ onOpenChange={setIsProjectDialogOpen}
116
+ onSave={saveProjectRoot}
117
+ />
118
+ ) : null}
119
+ </div>
120
+ );
121
+ }
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildChatWelcomeProjectOptions } from "@/features/chat/features/welcome/utils/chat-welcome-project-options.utils";
3
+ import type { NcpSessionSummaryView } from "@/shared/lib/api";
4
+
5
+ function createSummary(
6
+ overrides: Partial<NcpSessionSummaryView> & { sessionId: string },
7
+ ): NcpSessionSummaryView {
8
+ return {
9
+ sessionId: overrides.sessionId,
10
+ agentId: "main",
11
+ createdAt: overrides.createdAt ?? "2026-06-10T10:00:00.000Z",
12
+ updatedAt: overrides.updatedAt ?? "2026-06-10T10:00:00.000Z",
13
+ lastMessageAt: overrides.lastMessageAt,
14
+ messageCount: 1,
15
+ metadata: overrides.metadata ?? {},
16
+ status: "idle",
17
+ };
18
+ }
19
+
20
+ describe("buildChatWelcomeProjectOptions", () => {
21
+ it("builds recent project options from session summaries", () => {
22
+ const options = buildChatWelcomeProjectOptions({
23
+ defaultProjectRoot: "/Users/demo/.nextclaw/workspace",
24
+ sessionSummaries: [
25
+ createSummary({
26
+ sessionId: "session-1",
27
+ lastMessageAt: "2026-06-10T10:00:00.000Z",
28
+ metadata: { project_root: "/tmp/project-alpha" },
29
+ }),
30
+ createSummary({
31
+ sessionId: "session-2",
32
+ lastMessageAt: "2026-06-11T10:00:00.000Z",
33
+ metadata: { project_root: "/tmp/project-alpha" },
34
+ }),
35
+ ],
36
+ });
37
+
38
+ expect(options).toEqual([
39
+ {
40
+ projectRoot: "/Users/demo/.nextclaw/workspace",
41
+ projectName: "workspace",
42
+ sessionCount: 0,
43
+ isDefault: true,
44
+ },
45
+ {
46
+ projectRoot: "/tmp/project-alpha",
47
+ projectName: "project-alpha",
48
+ sessionCount: 2,
49
+ isDefault: false,
50
+ },
51
+ ]);
52
+ });
53
+ });
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ resolveChatWelcomeAgents,
4
+ resolveChatWelcomeSelectedAgent,
5
+ resolveChatWelcomeSelectedSessionType,
6
+ } from "@/features/chat/features/welcome/utils/chat-welcome-selection.utils";
7
+
8
+ const agents = [
9
+ { id: "main", displayName: "Main", runtime: "native" },
10
+ { id: "engineer", displayName: "Engineer", runtime: "codex" },
11
+ ];
12
+
13
+ describe("chat welcome selection utils", () => {
14
+ it("falls back to the selected agent when agent loading is empty", () => {
15
+ expect(
16
+ resolveChatWelcomeAgents({
17
+ agents: [],
18
+ fallbackAgentId: "main",
19
+ }),
20
+ ).toEqual([{ id: "main" }]);
21
+ });
22
+
23
+ it("resolves the selected welcome agent", () => {
24
+ expect(
25
+ resolveChatWelcomeSelectedAgent({
26
+ agents,
27
+ agentId: "engineer",
28
+ })?.runtime,
29
+ ).toBe("codex");
30
+ });
31
+
32
+ it("resolves the session type from the selected agent runtime", () => {
33
+ expect(
34
+ resolveChatWelcomeSelectedSessionType({
35
+ agents,
36
+ agentId: "engineer",
37
+ defaultSessionType: "native",
38
+ pendingSessionType: null,
39
+ selectedSessionType: null,
40
+ sessionTypeOptions: [
41
+ { value: "native" },
42
+ { value: "codex" },
43
+ ],
44
+ }),
45
+ ).toBe("codex");
46
+ });
47
+
48
+ it("prefers an explicit session type over the selected agent runtime", () => {
49
+ expect(
50
+ resolveChatWelcomeSelectedSessionType({
51
+ agents,
52
+ agentId: "engineer",
53
+ defaultSessionType: "native",
54
+ pendingSessionType: "native",
55
+ selectedSessionType: "native",
56
+ sessionTypeOptions: [
57
+ { value: "native" },
58
+ { value: "codex" },
59
+ ],
60
+ }),
61
+ ).toBe("native");
62
+ });
63
+ });