@nextclaw/ui 0.14.1-beta.0 → 0.14.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 (144) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/assets/{api-C6EDHBDR.js → api-DPcvCLfa.js} +3 -3
  3. package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-D6jtziu4.js} +2 -2
  4. package/dist/assets/chat-page-BBnNJoKG.js +105 -0
  5. package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-BCJhqdK9.js} +1 -1
  6. package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-CWh5FfK2.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-DsRhBizi.js} +1 -1
  8. package/dist/assets/{dist-CmMro3nV.js → dist-DPrgcEC0.js} +1 -1
  9. package/dist/assets/{dist-BIYV2-Ia.js → dist-DsE4OsSS.js} +1 -1
  10. package/dist/assets/doc-browser-C8xOF_9F.js +1 -0
  11. package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DgQedeAn.js} +1 -1
  12. package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-B46tQGmO.js} +1 -1
  13. package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-B81FStLm.js} +1 -1
  14. package/dist/assets/{external-link-D2cBOCqn.js → external-link-C_dmG-WJ.js} +1 -1
  15. package/dist/assets/index-7o1WdUbU.js +103 -0
  16. package/dist/assets/index-DQWmEWzk.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +40 -0
  19. package/dist/assets/model-config-page-Cv0O3BIj.js +1 -0
  20. package/dist/assets/plus-Bnt56oqG.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +1 -0
  22. package/dist/assets/providers-config-page-CPDz0Lxc.js +1 -0
  23. package/dist/assets/{react-DYQXXGbH.js → react-sZz5HSCm.js} +1 -1
  24. package/dist/assets/remote-B0LMs8u7.js +1 -0
  25. package/dist/assets/rotate-cw-BfyG_KEF.js +1 -0
  26. package/dist/assets/runtime-config-page-q0fvv8ME.js +1 -0
  27. package/dist/assets/{save-BNVTgBVn.js → save-MzjoiHri.js} +1 -1
  28. package/dist/assets/{search-CKCzwLId.js → search-B29dE4GN.js} +1 -1
  29. package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-Dl5sZzob.js} +1 -1
  30. package/dist/assets/secrets-config-page-C_l-IFCK.js +3 -0
  31. package/dist/assets/{select-CIRwTNP4.js → select-BKmh05hd.js} +2 -2
  32. package/dist/assets/{tag-chip-DKhVFtTE.js → tag-chip-BS_7Ueom.js} +1 -1
  33. package/dist/assets/use-config-DZkBWIdL.js +1 -0
  34. package/dist/assets/x-CW2KO-Hz.js +1 -0
  35. package/dist/index.html +14 -14
  36. package/package.json +9 -9
  37. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
  38. package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
  39. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
  40. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +70 -7
  41. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +104 -16
  42. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -2
  43. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +84 -6
  44. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
  45. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +88 -0
  46. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
  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 +21 -12
  49. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
  50. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
  51. package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
  52. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
  53. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
  54. package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
  55. package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
  56. package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
  57. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +3 -2
  58. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
  59. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
  60. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +24 -0
  61. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +27 -1
  62. package/src/features/chat/features/session-type/components/{chat-sidebar-create-menu.tsx → chat-session-type-menu.tsx} +17 -4
  63. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +15 -3
  64. package/src/features/chat/features/session-type/hooks/__tests__/use-chat-new-session-type-preference.test.tsx +152 -0
  65. package/src/features/chat/features/session-type/hooks/use-chat-new-session-type-preference.ts +159 -0
  66. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
  67. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
  68. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
  69. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
  70. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
  71. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
  72. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
  73. package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
  74. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
  75. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
  76. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
  77. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
  78. package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
  79. package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
  80. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
  81. package/src/features/chat/managers/chat-input.manager.ts +24 -2
  82. package/src/features/chat/stores/chat-input.store.ts +2 -0
  83. package/src/features/chat/types/chat-input-bar.types.ts +5 -0
  84. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +24 -0
  85. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +8 -3
  86. package/src/features/marketplace/components/marketplace-item-list-view.tsx +2 -2
  87. package/src/features/marketplace/components/marketplace-list-card.tsx +124 -98
  88. package/src/features/marketplace/components/marketplace-page-parts.tsx +11 -6
  89. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
  90. package/src/features/settings/components/config/provider-form.tsx +284 -387
  91. package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
  92. package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
  93. package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
  94. package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
  95. package/src/features/settings/pages/secrets-config-page.tsx +2 -1
  96. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
  97. package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
  98. package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
  99. package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
  100. package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
  101. package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
  102. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
  103. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
  104. package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
  105. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
  106. package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
  107. package/src/features/system-status/components/runtime-security-card.tsx +79 -25
  108. package/src/shared/components/common/__tests__/agent-avatar.test.tsx +3 -1
  109. package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
  110. package/src/shared/components/common/agent-avatar.tsx +5 -2
  111. package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
  112. package/src/shared/components/common/searchable-model-input.tsx +102 -87
  113. package/src/shared/components/ui/README.md +7 -0
  114. package/src/shared/components/ui/popover.tsx +19 -3
  115. package/src/shared/components/ui/select.tsx +14 -4
  116. package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
  117. package/src/shared/lib/api/index.ts +3 -0
  118. package/src/shared/lib/api/preferences/preference-keys.config.ts +9 -0
  119. package/src/shared/lib/api/preferences/preference.types.ts +22 -0
  120. package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
  121. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
  122. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  123. package/src/shared/lib/i18n/locales/en-US/marketplace.json +2 -0
  124. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
  125. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  126. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +2 -0
  127. package/dist/assets/chat-page-BXngLJY6.js +0 -105
  128. package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
  129. package/dist/assets/index-DTb_XVIH.js +0 -103
  130. package/dist/assets/index-ayiPYhf1.css +0 -1
  131. package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
  132. package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
  133. package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
  134. package/dist/assets/plus-CYE1onU2.js +0 -1
  135. package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
  136. package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
  137. package/dist/assets/remote-BlJDbXOI.js +0 -1
  138. package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
  139. package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
  140. package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
  141. package/dist/assets/use-config-DgKM9VIU.js +0 -1
  142. package/dist/assets/x-Cm3JjYut.js +0 -1
  143. package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
  144. package/src/features/chat/components/chat-welcome.tsx +0 -102
@@ -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
+ });
@@ -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
+ }