@nextclaw/ui 0.6.9 → 0.6.11

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 (83) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/CHANGELOG.md +15 -0
  3. package/dist/assets/{ChannelsList-DACqpUYZ.js → ChannelsList-C49JQ-Zt.js} +1 -1
  4. package/dist/assets/ChatPage-DIx05c6s.js +36 -0
  5. package/dist/assets/{DocBrowser-D7mjKkGe.js → DocBrowser-CpOosDEI.js} +1 -1
  6. package/dist/assets/{LogoBadge-BlDT-g9R.js → LogoBadge-CL_8ZPXU.js} +1 -1
  7. package/dist/assets/MarketplacePage-BOzko5s9.js +49 -0
  8. package/dist/assets/{ModelConfig-DwRU5qrw.js → ModelConfig-BZ4ZfaQB.js} +1 -1
  9. package/dist/assets/ProvidersList-fPpJ5gl6.js +1 -0
  10. package/dist/assets/{RuntimeConfig-C7BRLGSC.js → RuntimeConfig-Dt9pLB9P.js} +1 -1
  11. package/dist/assets/{SecretsConfig-D5xZh7VF.js → SecretsConfig-C1PU0Yy8.js} +2 -2
  12. package/dist/assets/{SessionsConfig-ovpj_otA.js → SessionsConfig-EskBOofQ.js} +2 -2
  13. package/dist/assets/{card-Bf4CtrW8.js → card-C7Gtw2Vs.js} +1 -1
  14. package/dist/assets/index-Cn6_2To7.js +8 -0
  15. package/dist/assets/index-nEYGCJTC.css +1 -0
  16. package/dist/assets/{input-CaKJyoWZ.js → input-oBvxsnV9.js} +1 -1
  17. package/dist/assets/{label-BaXSWTKI.js → label-C7F8lMpQ.js} +1 -1
  18. package/dist/assets/{page-layout-DA6PFRtQ.js → page-layout-DO8BlScF.js} +1 -1
  19. package/dist/assets/session-run-status-Kg0FwAPn.js +3 -0
  20. package/dist/assets/{switch-Cvd5wZs-.js → switch-C6a5GyZB.js} +1 -1
  21. package/dist/assets/{tabs-custom-0PybLkXs.js → tabs-custom-BatFap5k.js} +1 -1
  22. package/dist/assets/{useConfirmDialog-DdtpSju1.js → useConfirmDialog-zJzVKMdu.js} +2 -2
  23. package/dist/assets/{vendor-C--HHaLf.js → vendor-TlME1INH.js} +84 -84
  24. package/dist/index.html +3 -3
  25. package/package.json +4 -2
  26. package/src/App.tsx +1 -2
  27. package/src/api/config.ts +205 -202
  28. package/src/api/types.ts +54 -24
  29. package/src/components/chat/ChatConversationPanel.tsx +102 -121
  30. package/src/components/chat/ChatPage.tsx +165 -437
  31. package/src/components/chat/ChatSidebar.tsx +30 -36
  32. package/src/components/chat/ChatThread.tsx +73 -131
  33. package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
  34. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
  35. package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
  36. package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
  37. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
  38. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
  39. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
  40. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
  41. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
  42. package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
  43. package/src/components/chat/chat-input.types.ts +15 -0
  44. package/src/components/chat/chat-page-data.ts +121 -0
  45. package/src/components/chat/chat-page-runtime.ts +221 -0
  46. package/src/components/chat/chat-session-route.ts +59 -0
  47. package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
  48. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
  49. package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
  50. package/src/components/chat/chat-stream/transport.ts +159 -0
  51. package/src/components/chat/chat-stream/types.ts +76 -0
  52. package/src/components/chat/managers/chat-input.manager.ts +142 -0
  53. package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
  54. package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
  55. package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
  56. package/src/components/chat/managers/chat-thread.manager.ts +86 -0
  57. package/src/components/chat/managers/chat-ui.manager.ts +65 -0
  58. package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
  59. package/src/components/chat/presenter/chat.presenter.ts +32 -0
  60. package/src/components/chat/stores/chat-input.store.ts +62 -0
  61. package/src/components/chat/stores/chat-run-status.store.ts +30 -0
  62. package/src/components/chat/stores/chat-session-list.store.ts +34 -0
  63. package/src/components/chat/stores/chat-thread.store.ts +52 -0
  64. package/src/components/chat/useChatRuntimeController.ts +134 -0
  65. package/src/components/chat/useChatSessionTypeState.ts +148 -0
  66. package/src/components/common/MaskedInput.tsx +1 -1
  67. package/src/components/config/ProviderForm.tsx +221 -14
  68. package/src/hooks/useConfig.ts +33 -2
  69. package/src/hooks/useObservable.ts +20 -0
  70. package/src/hooks/useWebSocket.ts +23 -1
  71. package/src/lib/chat-message.ts +2 -202
  72. package/src/lib/chat-runtime-utils.ts +250 -0
  73. package/src/lib/i18n.ts +11 -0
  74. package/tsconfig.json +2 -1
  75. package/vite.config.ts +2 -1
  76. package/dist/assets/ChatPage-iji0RkTR.js +0 -34
  77. package/dist/assets/MarketplacePage-CZq3jVgg.js +0 -49
  78. package/dist/assets/ProvidersList-DFxN3pjx.js +0 -1
  79. package/dist/assets/index-C_DhisNo.css +0 -1
  80. package/dist/assets/index-dKTqKCJo.js +0 -7
  81. package/dist/assets/session-run-status-CllIZxNf.js +0 -5
  82. package/src/components/chat/ChatInputBar.tsx +0 -590
  83. package/src/components/chat/useChatStreamController.ts +0 -591
package/src/api/types.ts CHANGED
@@ -77,6 +77,7 @@ export type ProviderConnectionTestResult = {
77
77
  export type ProviderAuthStartResult = {
78
78
  provider: string;
79
79
  kind: "device_code";
80
+ methodId?: string;
80
81
  sessionId: string;
81
82
  verificationUri: string;
82
83
  userCode: string;
@@ -85,6 +86,10 @@ export type ProviderAuthStartResult = {
85
86
  note?: string;
86
87
  };
87
88
 
89
+ export type ProviderAuthStartRequest = {
90
+ methodId?: string;
91
+ };
92
+
88
93
  export type ProviderAuthPollRequest = {
89
94
  sessionId: string;
90
95
  };
@@ -141,6 +146,8 @@ export type SessionEntryView = {
141
146
  updatedAt: string;
142
147
  label?: string;
143
148
  preferredModel?: string;
149
+ sessionType: string;
150
+ sessionTypeMutable: boolean;
144
151
  messageCount: number;
145
152
  lastRole?: string;
146
153
  lastTimestamp?: string;
@@ -172,6 +179,8 @@ export type SessionHistoryView = {
172
179
  key: string;
173
180
  totalMessages: number;
174
181
  totalEvents: number;
182
+ sessionType: string;
183
+ sessionTypeMutable: boolean;
175
184
  metadata: Record<string, unknown>;
176
185
  messages: SessionMessageView[];
177
186
  events: SessionEventView[];
@@ -180,6 +189,7 @@ export type SessionHistoryView = {
180
189
  export type SessionPatchUpdate = {
181
190
  label?: string | null;
182
191
  preferredModel?: string | null;
192
+ sessionType?: string | null;
183
193
  clearHistory?: boolean;
184
194
  };
185
195
 
@@ -203,11 +213,42 @@ export type ChatTurnView = {
203
213
  durationMs: number;
204
214
  };
205
215
 
216
+ export type ChatTurnStreamReadyEvent = {
217
+ sessionKey: string;
218
+ requestedAt?: string;
219
+ runId?: string;
220
+ stopSupported?: boolean;
221
+ stopReason?: string;
222
+ };
223
+
224
+ export type ChatTurnStreamDeltaEvent = {
225
+ delta: string;
226
+ };
227
+
228
+ export type ChatTurnStreamSessionEvent = {
229
+ data: SessionEventView;
230
+ };
231
+
232
+ export type ChatTurnStreamErrorEvent = {
233
+ code?: string;
234
+ message?: string;
235
+ };
236
+
206
237
  export type ChatCapabilitiesView = {
207
238
  stopSupported: boolean;
208
239
  stopReason?: string;
209
240
  };
210
241
 
242
+ export type ChatSessionTypeOptionView = {
243
+ value: string;
244
+ label: string;
245
+ };
246
+
247
+ export type ChatSessionTypesView = {
248
+ defaultType: string;
249
+ options: ChatSessionTypeOptionView[];
250
+ };
251
+
211
252
  export type ChatCommandOptionView = {
212
253
  name: string;
213
254
  description: string;
@@ -262,30 +303,6 @@ export type ChatRunListView = {
262
303
  total: number;
263
304
  };
264
305
 
265
- export type ChatTurnStreamReadyEvent = {
266
- event: "ready";
267
- sessionKey: string;
268
- requestedAt: string;
269
- runId?: string;
270
- stopSupported?: boolean;
271
- stopReason?: string;
272
- };
273
-
274
- export type ChatTurnStreamDeltaEvent = {
275
- event: "delta";
276
- delta: string;
277
- };
278
-
279
- export type ChatTurnStreamSessionEvent = {
280
- event: "session_event";
281
- data: SessionEventView;
282
- };
283
-
284
- export type ChatTurnStreamFinalEvent = {
285
- event: "final";
286
- data: ChatTurnView;
287
- };
288
-
289
306
  export type CronScheduleView =
290
307
  | { kind: "at"; atMs?: number | null }
291
308
  | { kind: "every"; everyMs?: number | null }
@@ -458,6 +475,18 @@ export type ProviderSpecView = {
458
475
  en?: string;
459
476
  zh?: string;
460
477
  };
478
+ methods?: Array<{
479
+ id: string;
480
+ label?: {
481
+ en?: string;
482
+ zh?: string;
483
+ };
484
+ hint?: {
485
+ en?: string;
486
+ zh?: string;
487
+ };
488
+ }>;
489
+ defaultMethodId?: string;
461
490
  supportsCliImport?: boolean;
462
491
  };
463
492
  defaultModels?: string[];
@@ -559,6 +588,7 @@ export type ConfigActionExecuteResult = {
559
588
  export type WsEvent =
560
589
  | { type: 'config.updated'; payload: { path: string } }
561
590
  | { type: 'run.updated'; payload: { run: ChatRunView } }
591
+ | { type: 'session.updated'; payload: { sessionKey: string } }
562
592
  | { type: 'config.reload.started'; payload?: Record<string, unknown> }
563
593
  | { type: 'config.reload.finished'; payload?: Record<string, unknown> }
564
594
  | { type: 'error'; payload: { message: string; code?: string } }
@@ -1,44 +1,15 @@
1
- import type { MutableRefObject } from 'react';
2
- import type { MarketplaceInstalledRecord, SessionEventView } from '@/api/types';
1
+ import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
3
2
  import { Button } from '@/components/ui/button';
4
3
  import { ChatThread } from '@/components/chat/ChatThread';
5
- import { ChatInputBar, type ChatModelOption } from '@/components/chat/ChatInputBar';
6
4
  import { ChatWelcome } from '@/components/chat/ChatWelcome';
5
+ import { ChatInputBarView } from '@/components/chat/chat-input/ChatInputBarView';
6
+ import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
7
+ import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
7
8
  import { t } from '@/lib/i18n';
9
+ import { cn } from '@/lib/utils';
8
10
  import { Trash2 } from 'lucide-react';
9
11
 
10
- type ChatConversationPanelProps = {
11
- isProviderStateResolved: boolean;
12
- modelOptions: ChatModelOption[];
13
- selectedModel: string;
14
- onSelectedModelChange: (value: string) => void;
15
- onGoToProviders: () => void;
16
- skillRecords: MarketplaceInstalledRecord[];
17
- isSkillsLoading?: boolean;
18
- selectedSkills: string[];
19
- onSelectedSkillsChange: (next: string[]) => void;
20
- selectedSessionKey: string | null;
21
- sessionDisplayName?: string;
22
- canDeleteSession: boolean;
23
- isDeletePending: boolean;
24
- onDeleteSession: () => void;
25
- onCreateSession: () => void;
26
- threadRef: MutableRefObject<HTMLDivElement | null>;
27
- onThreadScroll: () => void;
28
- isHistoryLoading: boolean;
29
- mergedEvents: SessionEventView[];
30
- isSending: boolean;
31
- isAwaitingAssistantOutput: boolean;
32
- streamingAssistantText: string;
33
- draft: string;
34
- onDraftChange: (value: string) => void;
35
- onSend: () => Promise<void> | void;
36
- onStop: () => Promise<void> | void;
37
- canStopGeneration: boolean;
38
- stopDisabledReason?: string | null;
39
- sendError?: string | null;
40
- queuedCount: number;
41
- };
12
+ const STICKY_BOTTOM_THRESHOLD_PX = 10;
42
13
 
43
14
  function ChatConversationSkeleton() {
44
15
  return (
@@ -68,80 +39,101 @@ function ChatConversationSkeleton() {
68
39
  );
69
40
  }
70
41
 
71
- export function ChatConversationPanel({
72
- isProviderStateResolved,
73
- modelOptions,
74
- selectedModel,
75
- onSelectedModelChange,
76
- onGoToProviders,
77
- skillRecords,
78
- isSkillsLoading = false,
79
- selectedSkills,
80
- onSelectedSkillsChange,
81
- selectedSessionKey,
82
- sessionDisplayName,
83
- canDeleteSession,
84
- isDeletePending,
85
- onDeleteSession,
86
- onCreateSession,
87
- threadRef,
88
- onThreadScroll,
89
- isHistoryLoading,
90
- mergedEvents,
91
- isSending,
92
- isAwaitingAssistantOutput,
93
- streamingAssistantText,
94
- draft,
95
- onDraftChange,
96
- onSend,
97
- onStop,
98
- canStopGeneration,
99
- stopDisabledReason,
100
- sendError,
101
- queuedCount,
102
- }: ChatConversationPanelProps) {
103
- const showWelcome = !selectedSessionKey && mergedEvents.length === 0;
104
- const hasConfiguredModel = modelOptions.length > 0;
105
- const shouldShowProviderHint = isProviderStateResolved && !hasConfiguredModel;
42
+ export function ChatConversationPanel() {
43
+ const presenter = usePresenter();
44
+ const snapshot = useChatThreadStore((state) => state.snapshot);
45
+ const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
46
+ const threadRef = snapshot.threadRef ?? fallbackThreadRef;
47
+
48
+ // --- Sticky-to-bottom scroll state ---
49
+ const isStickyRef = useRef(true);
50
+ const isProgrammaticScrollRef = useRef(false);
51
+ const previousSessionKeyRef = useRef<string | null>(null);
52
+ const pendingInitialScrollRef = useRef(false);
53
+
54
+ const showWelcome = !snapshot.selectedSessionKey && snapshot.uiMessages.length === 0 && !snapshot.isSending;
55
+ const hasConfiguredModel = snapshot.modelOptions.length > 0;
56
+ const shouldShowProviderHint = snapshot.isProviderStateResolved && !hasConfiguredModel;
106
57
  const hideEmptyHint =
107
- isHistoryLoading &&
108
- mergedEvents.length === 0 &&
109
- !isSending &&
110
- !isAwaitingAssistantOutput &&
111
- !streamingAssistantText.trim();
58
+ snapshot.isHistoryLoading &&
59
+ snapshot.uiMessages.length === 0 &&
60
+ !snapshot.isSending &&
61
+ !snapshot.isAwaitingAssistantOutput;
62
+
63
+ const scrollToBottom = useCallback(() => {
64
+ const el = threadRef.current;
65
+ if (!el) return;
66
+ isProgrammaticScrollRef.current = true;
67
+ el.scrollTop = el.scrollHeight;
68
+ }, [threadRef]);
69
+
70
+ const handleScroll = useCallback(() => {
71
+ // Skip sticky check for programmatic scrolls
72
+ if (isProgrammaticScrollRef.current) {
73
+ isProgrammaticScrollRef.current = false;
74
+ return;
75
+ }
76
+ const el = threadRef.current;
77
+ if (!el) return;
78
+ const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
79
+ isStickyRef.current = distanceFromBottom <= STICKY_BOTTOM_THRESHOLD_PX;
80
+ }, [threadRef]);
81
+
82
+ // Session change → force sticky + schedule initial scroll
83
+ useEffect(() => {
84
+ if (previousSessionKeyRef.current === snapshot.selectedSessionKey) return;
85
+ previousSessionKeyRef.current = snapshot.selectedSessionKey;
86
+ isStickyRef.current = true;
87
+ pendingInitialScrollRef.current = true;
88
+ }, [snapshot.selectedSessionKey]);
89
+
90
+ // Initial scroll after history loads for a new session
91
+ useLayoutEffect(() => {
92
+ if (!pendingInitialScrollRef.current) return;
93
+ if (snapshot.isHistoryLoading || snapshot.uiMessages.length === 0) return;
94
+ pendingInitialScrollRef.current = false;
95
+ scrollToBottom();
96
+ }, [scrollToBottom, snapshot.isHistoryLoading, snapshot.uiMessages]);
97
+
98
+ // Streaming updates: keep bottom visible while still sticky.
99
+ useLayoutEffect(() => {
100
+ if (!isStickyRef.current) return;
101
+ if (snapshot.uiMessages.length === 0) return;
102
+ scrollToBottom();
103
+ }, [scrollToBottom, snapshot.uiMessages]);
112
104
 
113
- if (!isProviderStateResolved) {
105
+ if (!snapshot.isProviderStateResolved) {
114
106
  return <ChatConversationSkeleton />;
115
107
  }
116
108
 
117
109
  return (
118
110
  <section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
119
- {/* Minimal top bar - only shown when session is active */}
120
- {selectedSessionKey && (
121
- <div className="px-5 py-3 border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0">
122
- <div className="min-w-0 flex-1">
123
- <span className="text-sm font-medium text-gray-700 truncate">
124
- {sessionDisplayName || selectedSessionKey}
125
- </span>
126
- </div>
127
- <Button
128
- variant="ghost"
129
- size="icon"
130
- className="rounded-lg shrink-0 text-gray-400 hover:text-destructive"
131
- onClick={onDeleteSession}
132
- disabled={!canDeleteSession || isDeletePending}
133
- >
134
- <Trash2 className="h-4 w-4" />
135
- </Button>
111
+ <div className={cn(
112
+ "px-5 border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-all duration-200",
113
+ snapshot.selectedSessionKey ? "py-3 opacity-100" : "h-0 py-0 opacity-0 border-b-0"
114
+ )}>
115
+ <div className="min-w-0 flex-1">
116
+ <span className="text-sm font-medium text-gray-700 truncate">
117
+ {snapshot.sessionDisplayName || snapshot.selectedSessionKey}
118
+ </span>
136
119
  </div>
137
- )}
120
+ <Button
121
+ variant="ghost"
122
+ size="icon"
123
+ className="rounded-lg shrink-0 text-gray-400 hover:text-destructive"
124
+ onClick={presenter.chatThreadManager.deleteSession}
125
+ disabled={!snapshot.canDeleteSession || snapshot.isDeletePending}
126
+ >
127
+ <Trash2 className="h-4 w-4" />
128
+ </Button>
129
+ </div>
138
130
 
139
131
  {shouldShowProviderHint && (
140
132
  <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 flex items-center justify-between gap-3 shrink-0">
141
133
  <span className="text-xs text-amber-800">{t('chatModelNoOptions')}</span>
142
134
  <button
143
135
  type="button"
144
- onClick={onGoToProviders}
136
+ onClick={presenter.chatThreadManager.goToProviders}
145
137
  className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
146
138
  >
147
139
  {t('chatGoConfigureProvider')}
@@ -149,42 +141,31 @@ export function ChatConversationPanel({
149
141
  </div>
150
142
  )}
151
143
 
152
- {/* Message thread or welcome */}
153
- <div ref={threadRef} onScroll={onThreadScroll} className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
144
+ {snapshot.sessionTypeUnavailable && snapshot.sessionTypeUnavailableMessage?.trim() && (
145
+ <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 shrink-0">
146
+ <span className="text-xs text-amber-800">{snapshot.sessionTypeUnavailableMessage}</span>
147
+ </div>
148
+ )}
149
+
150
+ <div
151
+ ref={threadRef}
152
+ onScroll={handleScroll}
153
+ className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
154
+ >
154
155
  {showWelcome ? (
155
- <ChatWelcome onCreateSession={onCreateSession} />
156
+ <ChatWelcome onCreateSession={presenter.chatThreadManager.createSession} />
156
157
  ) : hideEmptyHint ? (
157
158
  <div className="h-full" />
158
- ) : mergedEvents.length === 0 ? (
159
+ ) : snapshot.uiMessages.length === 0 ? (
159
160
  <div className="px-5 py-5 text-sm text-gray-500">{t('chatNoMessages')}</div>
160
161
  ) : (
161
162
  <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
162
- <ChatThread events={mergedEvents} isSending={isSending && isAwaitingAssistantOutput} />
163
+ <ChatThread uiMessages={snapshot.uiMessages} isSending={snapshot.isSending && snapshot.isAwaitingAssistantOutput} />
163
164
  </div>
164
165
  )}
165
166
  </div>
166
167
 
167
- {/* Enhanced input bar */}
168
- <ChatInputBar
169
- isProviderStateResolved={isProviderStateResolved}
170
- draft={draft}
171
- onDraftChange={onDraftChange}
172
- onSend={onSend}
173
- onStop={onStop}
174
- onGoToProviders={onGoToProviders}
175
- canStopGeneration={canStopGeneration}
176
- stopDisabledReason={stopDisabledReason}
177
- sendError={sendError}
178
- isSending={isSending}
179
- queuedCount={queuedCount}
180
- modelOptions={modelOptions}
181
- selectedModel={selectedModel}
182
- onSelectedModelChange={onSelectedModelChange}
183
- skillRecords={skillRecords}
184
- isSkillsLoading={isSkillsLoading}
185
- selectedSkills={selectedSkills}
186
- onSelectedSkillsChange={onSelectedSkillsChange}
187
- />
168
+ <ChatInputBarView />
188
169
  </section>
189
170
  );
190
171
  }