@nextclaw/ui 0.6.15 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -0
  3. package/dist/assets/ChannelsList-DF2U-LY1.js +1 -0
  4. package/dist/assets/ChatPage-BX39y0U5.js +36 -0
  5. package/dist/assets/DocBrowser-B9ws5JL7.js +1 -0
  6. package/dist/assets/{LogoBadge-Cer0jX6t.js → LogoBadge-DvGAzkZ3.js} +1 -1
  7. package/dist/assets/MarketplacePage-DG5mHWJ8.js +49 -0
  8. package/dist/assets/ModelConfig-BL_HsOsm.js +1 -0
  9. package/dist/assets/ProvidersList-CH5z00YT.js +1 -0
  10. package/dist/assets/RuntimeConfig-BplBgkwo.js +1 -0
  11. package/dist/assets/SearchConfig-BhaI0fUf.js +1 -0
  12. package/dist/assets/{SecretsConfig-BnGVZiv4.js → SecretsConfig-CFoimOh9.js} +2 -2
  13. package/dist/assets/SessionsConfig-BHTAYn9T.js +2 -0
  14. package/dist/assets/index-BLeJkJ0o.css +1 -0
  15. package/dist/assets/index-DK4TS5ev.js +8 -0
  16. package/dist/assets/index-X5J6Mm--.js +1 -0
  17. package/dist/assets/{index-CkqvHQAt.js → index-uMsNsQX6.js} +1 -1
  18. package/dist/assets/{label-DkL14Jvl.js → label-D8ly4a2P.js} +1 -1
  19. package/dist/assets/page-layout-BSYfvwbp.js +1 -0
  20. package/dist/assets/security-config-DlKEYHNN.js +1 -0
  21. package/dist/assets/{session-run-status-tZ4ISNj-.js → session-run-status-TkIuGbVw.js} +1 -1
  22. package/dist/assets/skeleton-CWbsNx2h.js +1 -0
  23. package/dist/assets/{switch-CgbPbIX3.js → switch-Ce_g9lpN.js} +1 -1
  24. package/dist/assets/tabs-custom-Cf5azvT5.js +1 -0
  25. package/dist/assets/useConfirmDialog-A8Ek8Wu7.js +5 -0
  26. package/dist/assets/vendor-B7ozqnFC.js +412 -0
  27. package/dist/index.html +3 -3
  28. package/package.json +9 -5
  29. package/src/App.tsx +49 -27
  30. package/src/api/client.ts +1 -0
  31. package/src/api/config.ts +60 -0
  32. package/src/api/types.ts +26 -0
  33. package/src/components/auth/login-page.tsx +69 -0
  34. package/src/components/chat/ChatConversationPanel.tsx +12 -54
  35. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +80 -0
  36. package/src/components/chat/adapters/chat-input-bar.adapter.ts +329 -0
  37. package/src/components/chat/adapters/chat-message.adapter.test.ts +137 -0
  38. package/src/components/chat/adapters/chat-message.adapter.ts +200 -0
  39. package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +128 -0
  40. package/src/components/chat/chat-input/chat-input-bar.controller.ts +105 -0
  41. package/src/components/chat/containers/chat-input-bar.container.tsx +270 -0
  42. package/src/components/chat/containers/chat-message-list.container.tsx +67 -0
  43. package/src/components/chat/index.ts +1 -0
  44. package/src/components/chat/managers/chat-thread.manager.ts +3 -1
  45. package/src/components/chat/nextclaw/index.ts +23 -0
  46. package/src/components/config/runtime-security-card.tsx +276 -0
  47. package/src/components/config/security-config.tsx +12 -0
  48. package/src/components/layout/Sidebar.tsx +6 -1
  49. package/src/components/marketplace/MarketplacePage.test.tsx +170 -0
  50. package/src/components/marketplace/MarketplacePage.tsx +77 -28
  51. package/src/hooks/use-auth.ts +111 -0
  52. package/src/hooks/useMarketplace.ts +9 -0
  53. package/src/lib/i18n.ts +72 -0
  54. package/src/test/setup.ts +16 -0
  55. package/tsconfig.json +3 -2
  56. package/vite.config.ts +2 -1
  57. package/vitest.config.ts +16 -0
  58. package/dist/assets/ChannelsList-DzeVn-JC.js +0 -1
  59. package/dist/assets/ChatPage-BiFhIm1-.js +0 -36
  60. package/dist/assets/DocBrowser-By3lF9yN.js +0 -1
  61. package/dist/assets/MarketplacePage-EZxALdIz.js +0 -49
  62. package/dist/assets/ModelConfig-AchYxLft.js +0 -1
  63. package/dist/assets/ProvidersList-BsD-4kKX.js +0 -1
  64. package/dist/assets/RuntimeConfig-sKOERbFD.js +0 -1
  65. package/dist/assets/SearchConfig-DAfvDwX6.js +0 -1
  66. package/dist/assets/SessionsConfig-CzvrKDRs.js +0 -2
  67. package/dist/assets/card-BAM7vbMg.js +0 -1
  68. package/dist/assets/index-D9rRqOi8.css +0 -1
  69. package/dist/assets/index-DJZ5y7t1.js +0 -8
  70. package/dist/assets/input-BoelTiYL.js +0 -1
  71. package/dist/assets/page-layout-CERNdqzB.js +0 -1
  72. package/dist/assets/popover-uwYz3Chm.js +0 -1
  73. package/dist/assets/tabs-custom-pDyl95el.js +0 -1
  74. package/dist/assets/useConfirmDialog-DyP6Ac75.js +0 -5
  75. package/dist/assets/vendor-BKtTvQYU.js +0 -407
  76. package/src/components/chat/ChatThread.tsx +0 -402
  77. package/src/components/chat/SkillsPicker.tsx +0 -137
  78. package/src/components/chat/chat-input/ChatInputBarView.tsx +0 -82
  79. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +0 -83
  80. package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +0 -39
  81. package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +0 -31
  82. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +0 -112
  83. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +0 -24
  84. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +0 -58
  85. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +0 -56
  86. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +0 -40
  87. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +0 -74
  88. package/src/components/chat/chat-input/useChatInputBarController.ts +0 -322
@@ -0,0 +1,128 @@
1
+ import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
2
+ import { renderHook, act } from '@testing-library/react';
3
+ import { useChatInputBarController } from '@/components/chat/chat-input/chat-input-bar.controller';
4
+
5
+ function createKeyEvent(
6
+ key: string,
7
+ overrides?: Partial<ReactKeyboardEvent<HTMLTextAreaElement>>
8
+ ): ReactKeyboardEvent<HTMLTextAreaElement> {
9
+ return {
10
+ key,
11
+ code: key === ' ' ? 'Space' : key,
12
+ shiftKey: false,
13
+ nativeEvent: {
14
+ isComposing: false
15
+ },
16
+ preventDefault: vi.fn(),
17
+ ...overrides
18
+ } as unknown as ReactKeyboardEvent<HTMLTextAreaElement>;
19
+ }
20
+
21
+ describe('useChatInputBarController', () => {
22
+ it('cycles slash items with arrow keys and selects the active item on enter', () => {
23
+ const onSelectSlashItem = vi.fn();
24
+ const { result } = renderHook(() =>
25
+ useChatInputBarController({
26
+ isSlashMode: true,
27
+ slashItems: [
28
+ { key: 'one', title: 'One', subtitle: 'Skill', description: '', detailLines: [] },
29
+ { key: 'two', title: 'Two', subtitle: 'Skill', description: '', detailLines: [] }
30
+ ],
31
+ isSlashLoading: false,
32
+ onSelectSlashItem,
33
+ onSend: vi.fn(),
34
+ onStop: vi.fn(),
35
+ isSending: false,
36
+ canStopGeneration: false
37
+ })
38
+ );
39
+
40
+ act(() => {
41
+ result.current.onTextareaKeyDown(createKeyEvent('ArrowDown'));
42
+ });
43
+ expect(result.current.activeSlashIndex).toBe(1);
44
+
45
+ act(() => {
46
+ result.current.onTextareaKeyDown(createKeyEvent('Enter'));
47
+ });
48
+ expect(onSelectSlashItem).toHaveBeenCalledWith(
49
+ expect.objectContaining({
50
+ key: 'two'
51
+ })
52
+ );
53
+ });
54
+
55
+ it('dismisses the slash panel on space and triggers stop on escape outside slash mode', () => {
56
+ const onStop = vi.fn();
57
+ const { result, rerender } = renderHook(
58
+ (props: {
59
+ isSlashMode: boolean;
60
+ isSending: boolean;
61
+ canStopGeneration: boolean;
62
+ }) =>
63
+ useChatInputBarController({
64
+ isSlashMode: props.isSlashMode,
65
+ slashItems: [{ key: 'one', title: 'One', subtitle: 'Skill', description: '', detailLines: [] }],
66
+ isSlashLoading: false,
67
+ onSelectSlashItem: vi.fn(),
68
+ onSend: vi.fn(),
69
+ onStop,
70
+ isSending: props.isSending,
71
+ canStopGeneration: props.canStopGeneration
72
+ }),
73
+ {
74
+ initialProps: {
75
+ isSlashMode: true,
76
+ isSending: false,
77
+ canStopGeneration: false
78
+ }
79
+ }
80
+ );
81
+
82
+ act(() => {
83
+ result.current.onTextareaKeyDown(createKeyEvent(' '));
84
+ });
85
+ expect(result.current.isSlashPanelOpen).toBe(false);
86
+
87
+ rerender({
88
+ isSlashMode: false,
89
+ isSending: true,
90
+ canStopGeneration: true
91
+ });
92
+
93
+ act(() => {
94
+ result.current.onTextareaKeyDown(createKeyEvent('Escape'));
95
+ });
96
+ expect(onStop).toHaveBeenCalled();
97
+ });
98
+
99
+ it('supports Tab selection and Enter send when slash mode is not active', () => {
100
+ const onSelectSlashItem = vi.fn();
101
+ const onSend = vi.fn();
102
+ const { result, rerender } = renderHook(
103
+ (props: { isSlashMode: boolean }) =>
104
+ useChatInputBarController({
105
+ isSlashMode: props.isSlashMode,
106
+ slashItems: [{ key: 'one', title: 'One', subtitle: 'Skill', description: '', detailLines: [] }],
107
+ isSlashLoading: false,
108
+ onSelectSlashItem,
109
+ onSend,
110
+ onStop: vi.fn(),
111
+ isSending: false,
112
+ canStopGeneration: false
113
+ }),
114
+ { initialProps: { isSlashMode: true } }
115
+ );
116
+
117
+ act(() => {
118
+ result.current.onTextareaKeyDown(createKeyEvent('Tab'));
119
+ });
120
+ expect(onSelectSlashItem).toHaveBeenCalledTimes(1);
121
+
122
+ rerender({ isSlashMode: false });
123
+ act(() => {
124
+ result.current.onTextareaKeyDown(createKeyEvent('Enter'));
125
+ });
126
+ expect(onSend).toHaveBeenCalledTimes(1);
127
+ });
128
+ });
@@ -0,0 +1,105 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import type { ChatSlashItem } from '@nextclaw/agent-chat-ui';
3
+ import type { KeyboardEvent } from 'react';
4
+
5
+ type UseChatInputBarControllerParams = {
6
+ isSlashMode: boolean;
7
+ slashItems: ChatSlashItem[];
8
+ isSlashLoading: boolean;
9
+ onSelectSlashItem: (item: ChatSlashItem) => void;
10
+ onSend: () => Promise<void> | void;
11
+ onStop: () => Promise<void> | void;
12
+ isSending: boolean;
13
+ canStopGeneration: boolean;
14
+ };
15
+
16
+ export function useChatInputBarController(params: UseChatInputBarControllerParams) {
17
+ const [activeSlashIndex, setActiveSlashIndex] = useState(0);
18
+ const [dismissedSlashPanel, setDismissedSlashPanel] = useState(false);
19
+
20
+ const isSlashPanelOpen = params.isSlashMode && !dismissedSlashPanel;
21
+ const activeSlashItem = params.slashItems[activeSlashIndex] ?? null;
22
+
23
+ useEffect(() => {
24
+ if (!isSlashPanelOpen || params.slashItems.length === 0) {
25
+ setActiveSlashIndex(0);
26
+ return;
27
+ }
28
+ setActiveSlashIndex((current) => {
29
+ if (current < 0) {
30
+ return 0;
31
+ }
32
+ if (current >= params.slashItems.length) {
33
+ return params.slashItems.length - 1;
34
+ }
35
+ return current;
36
+ });
37
+ }, [isSlashPanelOpen, params.slashItems.length]);
38
+
39
+ useEffect(() => {
40
+ if (!params.isSlashMode && dismissedSlashPanel) {
41
+ setDismissedSlashPanel(false);
42
+ }
43
+ }, [dismissedSlashPanel, params.isSlashMode]);
44
+
45
+ const handleSelectSlashItem = useCallback((item: ChatSlashItem) => {
46
+ params.onSelectSlashItem(item);
47
+ setDismissedSlashPanel(false);
48
+ }, [params]);
49
+
50
+ const onTextareaKeyDown = useCallback((event: KeyboardEvent<HTMLTextAreaElement>) => {
51
+ if (isSlashPanelOpen && !event.nativeEvent.isComposing && (event.key === ' ' || event.code === 'Space')) {
52
+ setDismissedSlashPanel(true);
53
+ }
54
+ if (isSlashPanelOpen && params.slashItems.length > 0) {
55
+ if (event.key === 'ArrowDown') {
56
+ event.preventDefault();
57
+ setActiveSlashIndex((current) => (current + 1) % params.slashItems.length);
58
+ return;
59
+ }
60
+ if (event.key === 'ArrowUp') {
61
+ event.preventDefault();
62
+ setActiveSlashIndex((current) => (current - 1 + params.slashItems.length) % params.slashItems.length);
63
+ return;
64
+ }
65
+ if ((event.key === 'Enter' && !event.shiftKey) || event.key === 'Tab') {
66
+ event.preventDefault();
67
+ const selected = params.slashItems[activeSlashIndex];
68
+ if (selected) {
69
+ handleSelectSlashItem(selected);
70
+ }
71
+ return;
72
+ }
73
+ }
74
+ if (event.key === 'Escape') {
75
+ if (isSlashPanelOpen) {
76
+ event.preventDefault();
77
+ setDismissedSlashPanel(true);
78
+ return;
79
+ }
80
+ if (params.isSending && params.canStopGeneration) {
81
+ event.preventDefault();
82
+ void params.onStop();
83
+ return;
84
+ }
85
+ }
86
+ if (event.key === 'Enter' && !event.shiftKey) {
87
+ event.preventDefault();
88
+ void params.onSend();
89
+ }
90
+ }, [activeSlashIndex, handleSelectSlashItem, isSlashPanelOpen, params]);
91
+
92
+ return {
93
+ isSlashPanelOpen,
94
+ activeSlashIndex,
95
+ activeSlashItem,
96
+ onSelectSlashItem: handleSelectSlashItem,
97
+ onSlashPanelOpenChange: (open: boolean) => {
98
+ if (!open) {
99
+ setDismissedSlashPanel(true);
100
+ }
101
+ },
102
+ onSetActiveSlashIndex: setActiveSlashIndex,
103
+ onTextareaKeyDown
104
+ };
105
+ }
@@ -0,0 +1,270 @@
1
+ import { useMemo } from 'react';
2
+ import { ChatInputBar } from '@nextclaw/agent-chat-ui';
3
+ import {
4
+ buildChatSlashItems,
5
+ buildModelStateHint,
6
+ buildModelToolbarSelect,
7
+ buildSelectedSkillItems,
8
+ buildSessionTypeToolbarSelect,
9
+ buildSkillPickerModel,
10
+ buildThinkingToolbarSelect,
11
+ resolveSlashQuery,
12
+ type ChatModelRecord,
13
+ type ChatSkillRecord,
14
+ type ChatThinkingLevel
15
+ } from '@/components/chat/adapters/chat-input-bar.adapter';
16
+ import { useChatInputBarController } from '@/components/chat/chat-input/chat-input-bar.controller';
17
+ import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
18
+ import { useI18n } from '@/components/providers/I18nProvider';
19
+ import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
20
+ import { t } from '@/lib/i18n';
21
+
22
+ function buildThinkingLabels(): Record<ChatThinkingLevel, string> {
23
+ return {
24
+ off: t('chatThinkingLevelOff'),
25
+ minimal: t('chatThinkingLevelMinimal'),
26
+ low: t('chatThinkingLevelLow'),
27
+ medium: t('chatThinkingLevelMedium'),
28
+ high: t('chatThinkingLevelHigh'),
29
+ adaptive: t('chatThinkingLevelAdaptive'),
30
+ xhigh: t('chatThinkingLevelXhigh')
31
+ };
32
+ }
33
+
34
+ function toSkillRecords(snapshotRecords: Array<{
35
+ spec: string;
36
+ label?: string;
37
+ description?: string;
38
+ descriptionZh?: string;
39
+ origin?: string;
40
+ }>, officialBadgeLabel: string): ChatSkillRecord[] {
41
+ return snapshotRecords.map((record) => ({
42
+ key: record.spec,
43
+ label: record.label || record.spec,
44
+ description: record.description,
45
+ descriptionZh: record.descriptionZh,
46
+ badgeLabel: record.origin === 'builtin' ? officialBadgeLabel : undefined
47
+ }));
48
+ }
49
+
50
+ function toModelRecords(snapshotModels: Array<{
51
+ value: string;
52
+ modelLabel: string;
53
+ providerLabel: string;
54
+ thinkingCapability?: {
55
+ supported: string[];
56
+ default?: string | null;
57
+ } | null;
58
+ }>): ChatModelRecord[] {
59
+ return snapshotModels.map((model) => ({
60
+ value: model.value,
61
+ modelLabel: model.modelLabel,
62
+ providerLabel: model.providerLabel,
63
+ thinkingCapability: model.thinkingCapability
64
+ ? {
65
+ supported: model.thinkingCapability.supported as ChatThinkingLevel[],
66
+ default: (model.thinkingCapability.default as ChatThinkingLevel | null | undefined) ?? null
67
+ }
68
+ : null
69
+ }));
70
+ }
71
+
72
+ export function ChatInputBarContainer() {
73
+ const presenter = usePresenter();
74
+ const { language } = useI18n();
75
+ const snapshot = useChatInputStore((state) => state.snapshot);
76
+
77
+ const officialSkillBadgeLabel = useMemo(() => {
78
+ // Keep memo reactive to locale switches even though `t` is imported as a stable function.
79
+ const locale = language;
80
+ void locale;
81
+ return t('chatSkillsPickerOfficial');
82
+ }, [language]);
83
+ const slashTexts = useMemo(
84
+ () => {
85
+ // Keep memo reactive to locale switches even though `t` is imported as a stable function.
86
+ const locale = language;
87
+ void locale;
88
+ return {
89
+ slashSkillSubtitle: t('chatSlashTypeSkill'),
90
+ slashSkillSpecLabel: t('chatSlashSkillSpec'),
91
+ noSkillDescription: t('chatSkillsPickerNoDescription')
92
+ };
93
+ },
94
+ [language]
95
+ );
96
+
97
+ const skillRecords = useMemo(
98
+ () => toSkillRecords(snapshot.skillRecords, officialSkillBadgeLabel),
99
+ [snapshot.skillRecords, officialSkillBadgeLabel]
100
+ );
101
+ const modelRecords = useMemo(() => toModelRecords(snapshot.modelOptions), [snapshot.modelOptions]);
102
+
103
+ const hasModelOptions = modelRecords.length > 0;
104
+ const isModelOptionsLoading = !snapshot.isProviderStateResolved && !hasModelOptions;
105
+ const isModelOptionsEmpty = snapshot.isProviderStateResolved && !hasModelOptions;
106
+ const inputDisabled =
107
+ ((isModelOptionsLoading || isModelOptionsEmpty) && !snapshot.isSending) || snapshot.sessionTypeUnavailable;
108
+ const textareaPlaceholder = isModelOptionsLoading
109
+ ? ''
110
+ : hasModelOptions
111
+ ? t('chatInputPlaceholder')
112
+ : t('chatModelNoOptions');
113
+
114
+ const slashQuery = resolveSlashQuery(snapshot.draft);
115
+ const slashItems = useMemo(
116
+ () => buildChatSlashItems(skillRecords, slashQuery ?? '', slashTexts),
117
+ [slashQuery, skillRecords, slashTexts]
118
+ );
119
+
120
+ const controller = useChatInputBarController({
121
+ isSlashMode: slashQuery !== null,
122
+ slashItems,
123
+ isSlashLoading: snapshot.isSkillsLoading,
124
+ onSelectSlashItem: (item) => {
125
+ if (!item.value) {
126
+ return;
127
+ }
128
+ if (!snapshot.selectedSkills.includes(item.value)) {
129
+ presenter.chatInputManager.selectSkills([...snapshot.selectedSkills, item.value]);
130
+ }
131
+ presenter.chatInputManager.setDraft('');
132
+ },
133
+ onSend: presenter.chatInputManager.send,
134
+ onStop: presenter.chatInputManager.stop,
135
+ isSending: snapshot.isSending,
136
+ canStopGeneration: snapshot.canStopGeneration
137
+ });
138
+
139
+ const selectedSessionTypeOption =
140
+ snapshot.sessionTypeOptions.find((option) => option.value === snapshot.selectedSessionType) ??
141
+ (snapshot.selectedSessionType
142
+ ? { value: snapshot.selectedSessionType, label: snapshot.selectedSessionType }
143
+ : null);
144
+ const shouldShowSessionTypeSelector =
145
+ snapshot.canEditSessionType &&
146
+ (snapshot.sessionTypeOptions.length > 1 ||
147
+ Boolean(snapshot.selectedSessionType && snapshot.selectedSessionType !== 'native'));
148
+
149
+ const selectedModelOption = modelRecords.find((option) => option.value === snapshot.selectedModel);
150
+ const selectedModelThinkingCapability = selectedModelOption?.thinkingCapability;
151
+ const thinkingSupportedLevels = selectedModelThinkingCapability?.supported ?? [];
152
+
153
+ const resolvedStopHint =
154
+ snapshot.stopDisabledReason === '__preparing__'
155
+ ? t('chatStopPreparing')
156
+ : snapshot.stopDisabledReason?.trim() || t('chatStopUnavailable');
157
+
158
+ const toolbarSelects = [
159
+ buildSessionTypeToolbarSelect({
160
+ selectedSessionType: snapshot.selectedSessionType,
161
+ selectedSessionTypeOption,
162
+ sessionTypeOptions: snapshot.sessionTypeOptions,
163
+ onValueChange: presenter.chatInputManager.selectSessionType,
164
+ canEditSessionType: snapshot.canEditSessionType,
165
+ shouldShow: shouldShowSessionTypeSelector,
166
+ texts: {
167
+ sessionTypePlaceholder: t('chatSessionTypeLabel')
168
+ }
169
+ }),
170
+ buildModelToolbarSelect({
171
+ modelOptions: modelRecords,
172
+ selectedModel: snapshot.selectedModel,
173
+ isModelOptionsLoading,
174
+ hasModelOptions,
175
+ onValueChange: presenter.chatInputManager.selectModel,
176
+ texts: {
177
+ modelSelectPlaceholder: t('chatSelectModel'),
178
+ modelNoOptionsLabel: t('chatModelNoOptions')
179
+ }
180
+ }),
181
+ buildThinkingToolbarSelect({
182
+ supportedLevels: thinkingSupportedLevels,
183
+ selectedThinkingLevel: snapshot.selectedThinkingLevel as ChatThinkingLevel | null,
184
+ defaultThinkingLevel: selectedModelThinkingCapability?.default ?? null,
185
+ onValueChange: (value) => presenter.chatInputManager.selectThinkingLevel(value),
186
+ texts: {
187
+ thinkingLabels: buildThinkingLabels()
188
+ }
189
+ })
190
+ ].filter((item): item is NonNullable<typeof item> => item !== null);
191
+
192
+ const skillPicker = buildSkillPickerModel({
193
+ skillRecords,
194
+ selectedSkills: snapshot.selectedSkills,
195
+ isLoading: snapshot.isSkillsLoading,
196
+ onSelectedKeysChange: presenter.chatInputManager.selectSkills,
197
+ texts: {
198
+ title: t('chatSkillsPickerTitle'),
199
+ searchPlaceholder: t('chatSkillsPickerSearchPlaceholder'),
200
+ emptyLabel: t('chatSkillsPickerEmpty'),
201
+ loadingLabel: t('sessionsLoading'),
202
+ manageLabel: t('chatSkillsPickerManage')
203
+ }
204
+ });
205
+
206
+ return (
207
+ <ChatInputBar
208
+ value={snapshot.draft}
209
+ placeholder={textareaPlaceholder}
210
+ disabled={inputDisabled}
211
+ onValueChange={presenter.chatInputManager.setDraft}
212
+ onKeyDown={controller.onTextareaKeyDown}
213
+ slashMenu={{
214
+ isOpen: controller.isSlashPanelOpen,
215
+ isLoading: snapshot.isSkillsLoading,
216
+ items: slashItems,
217
+ activeIndex: controller.activeSlashIndex,
218
+ activeItem: controller.activeSlashItem,
219
+ texts: {
220
+ slashLoadingLabel: t('chatSlashLoading'),
221
+ slashSectionLabel: t('chatSlashSectionSkills'),
222
+ slashEmptyLabel: t('chatSlashNoResult'),
223
+ slashHintLabel: t('chatSlashHint'),
224
+ slashSkillHintLabel: t('chatSlashSkillHint')
225
+ },
226
+ onSelectItem: controller.onSelectSlashItem,
227
+ onOpenChange: controller.onSlashPanelOpenChange,
228
+ onSetActiveIndex: controller.onSetActiveSlashIndex
229
+ }}
230
+ hint={buildModelStateHint({
231
+ isModelOptionsLoading,
232
+ isModelOptionsEmpty,
233
+ onGoToProviders: presenter.chatInputManager.goToProviders,
234
+ texts: {
235
+ noModelOptionsLabel: t('chatModelNoOptions'),
236
+ configureProviderLabel: t('chatGoConfigureProvider')
237
+ }
238
+ })}
239
+ selectedItems={{
240
+ items: buildSelectedSkillItems(snapshot.selectedSkills, skillRecords),
241
+ onRemove: (key) => presenter.chatInputManager.selectSkills(snapshot.selectedSkills.filter((skill) => skill !== key))
242
+ }}
243
+ toolbar={{
244
+ selects: toolbarSelects,
245
+ accessories: [
246
+ {
247
+ key: 'attach',
248
+ label: t('chatInputAttachComingSoon'),
249
+ icon: 'paperclip',
250
+ disabled: true,
251
+ tooltip: t('chatInputAttachComingSoon')
252
+ }
253
+ ],
254
+ skillPicker,
255
+ actions: {
256
+ sendError: snapshot.sendError,
257
+ isSending: snapshot.isSending,
258
+ canStopGeneration: snapshot.canStopGeneration,
259
+ sendDisabled: snapshot.draft.trim().length === 0 || !hasModelOptions || snapshot.sessionTypeUnavailable,
260
+ stopDisabled: !snapshot.canStopGeneration,
261
+ stopHint: resolvedStopHint,
262
+ sendButtonLabel: t('chatSend'),
263
+ stopButtonLabel: t('chatStop'),
264
+ onSend: presenter.chatInputManager.send,
265
+ onStop: presenter.chatInputManager.stop
266
+ }
267
+ }}
268
+ />
269
+ );
270
+ }
@@ -0,0 +1,67 @@
1
+ import { useMemo } from 'react';
2
+ import { type UiMessage } from '@nextclaw/agent-chat';
3
+ import { ChatMessageList } from '@nextclaw/agent-chat-ui';
4
+ import { adaptChatMessages, type ChatMessageSource } from '@/components/chat/adapters/chat-message.adapter';
5
+ import { useI18n } from '@/components/providers/I18nProvider';
6
+ import { formatDateTime, t } from '@/lib/i18n';
7
+
8
+ type ChatMessageListContainerProps = {
9
+ uiMessages: UiMessage[];
10
+ isSending: boolean;
11
+ className?: string;
12
+ };
13
+
14
+ export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
15
+ const { language } = useI18n();
16
+ const sourceMessages = useMemo<ChatMessageSource[]>(
17
+ () =>
18
+ props.uiMessages.map((message) => ({
19
+ id: message.id,
20
+ role: message.role,
21
+ meta: {
22
+ timestamp: message.meta?.timestamp,
23
+ status: message.meta?.status
24
+ },
25
+ parts: message.parts as unknown as ChatMessageSource['parts']
26
+ })),
27
+ [props.uiMessages]
28
+ );
29
+
30
+ const messages = useMemo(
31
+ () =>
32
+ adaptChatMessages({
33
+ uiMessages: sourceMessages,
34
+ formatTimestamp: (value) => formatDateTime(value, language),
35
+ texts: {
36
+ roleLabels: {
37
+ user: t('chatRoleUser'),
38
+ assistant: t('chatRoleAssistant'),
39
+ tool: t('chatRoleTool'),
40
+ system: t('chatRoleSystem'),
41
+ fallback: t('chatRoleMessage')
42
+ },
43
+ reasoningLabel: t('chatReasoning'),
44
+ toolCallLabel: t('chatToolCall'),
45
+ toolResultLabel: t('chatToolResult'),
46
+ toolNoOutputLabel: t('chatToolNoOutput'),
47
+ toolOutputLabel: t('chatToolOutput'),
48
+ unknownPartLabel: t('chatUnknownPart')
49
+ }
50
+ }),
51
+ [language, sourceMessages]
52
+ );
53
+
54
+ return (
55
+ <ChatMessageList
56
+ messages={messages}
57
+ isSending={props.isSending}
58
+ hasStreamingDraft={props.uiMessages.some((message) => message.meta?.status === 'streaming')}
59
+ className={props.className}
60
+ texts={{
61
+ copyCodeLabel: t('chatCodeCopy'),
62
+ copiedCodeLabel: t('chatCodeCopied'),
63
+ typingLabel: t('chatTyping')
64
+ }}
65
+ />
66
+ );
67
+ }
@@ -0,0 +1 @@
1
+ export * from '@nextclaw/agent-chat-ui';
@@ -60,7 +60,9 @@ export class ChatThreadManager {
60
60
  };
61
61
 
62
62
  private deleteCurrentSession = async () => {
63
- const selectedSessionKey = useChatSessionListStore.getState().snapshot.selectedSessionKey;
63
+ const {
64
+ snapshot: { selectedSessionKey }
65
+ } = useChatSessionListStore.getState();
64
66
  if (!selectedSessionKey) {
65
67
  return;
66
68
  }
@@ -0,0 +1,23 @@
1
+ export { ChatInputBarContainer } from '@/components/chat/containers/chat-input-bar.container';
2
+ export { ChatMessageListContainer } from '@/components/chat/containers/chat-message-list.container';
3
+
4
+ export {
5
+ adaptChatMessages,
6
+ type ChatMessageAdapterTexts,
7
+ type ChatMessageSource,
8
+ type ChatMessagePartSource
9
+ } from '@/components/chat/adapters/chat-message.adapter';
10
+
11
+ export {
12
+ buildChatSlashItems,
13
+ buildSelectedSkillItems,
14
+ buildSkillPickerModel,
15
+ buildModelStateHint,
16
+ buildModelToolbarSelect,
17
+ buildSessionTypeToolbarSelect,
18
+ buildThinkingToolbarSelect,
19
+ resolveSlashQuery,
20
+ type ChatSkillRecord,
21
+ type ChatModelRecord,
22
+ type ChatThinkingLevel
23
+ } from '@/components/chat/adapters/chat-input-bar.adapter';