@nextclaw/ui 0.6.15 → 0.8.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 (103) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +2 -0
  3. package/dist/assets/ChannelsList-DBcoVJRW.js +1 -0
  4. package/dist/assets/ChatPage-CD3cxyyM.js +37 -0
  5. package/dist/assets/DocBrowser-DDX2HMXW.js +1 -0
  6. package/dist/assets/{LogoBadge-Cer0jX6t.js → LogoBadge-J53F_3JA.js} +1 -1
  7. package/dist/assets/MarketplacePage-0BZ4bza0.js +49 -0
  8. package/dist/assets/ModelConfig-Wzq9wGHV.js +1 -0
  9. package/dist/assets/ProvidersList-kwzRS8_M.js +1 -0
  10. package/dist/assets/RuntimeConfig-N771_AM6.js +1 -0
  11. package/dist/assets/SearchConfig-DVt5QVa_.js +1 -0
  12. package/dist/assets/{SecretsConfig-BnGVZiv4.js → SecretsConfig-CkwauPa8.js} +2 -2
  13. package/dist/assets/SessionsConfig-C3mnHzkZ.js +2 -0
  14. package/dist/assets/{session-run-status-tZ4ISNj-.js → chat-message-pxr79GDs.js} +3 -3
  15. package/dist/assets/index-BIvFMkN4.js +1 -0
  16. package/dist/assets/index-CzkY1reu.js +8 -0
  17. package/dist/assets/{index-CkqvHQAt.js → index-GdpEEKnz.js} +1 -1
  18. package/dist/assets/index-RZ0kHHRI.css +1 -0
  19. package/dist/assets/{label-DkL14Jvl.js → label-CmksBHgc.js} +1 -1
  20. package/dist/assets/page-layout-Db0GbnhS.js +1 -0
  21. package/dist/assets/security-config-CjLFME5Q.js +1 -0
  22. package/dist/assets/skeleton-CkpQeVWN.js +1 -0
  23. package/dist/assets/{switch-CgbPbIX3.js → switch-C24d-UJU.js} +1 -1
  24. package/dist/assets/tabs-custom-D89bh-fc.js +1 -0
  25. package/dist/assets/useConfirmDialog-BeP35LcG.js +5 -0
  26. package/dist/assets/vendor-psXJBy9u.js +407 -0
  27. package/dist/index.html +3 -3
  28. package/package.json +12 -5
  29. package/src/App.tsx +49 -27
  30. package/src/api/client.ts +1 -0
  31. package/src/api/config.ts +98 -0
  32. package/src/api/types.ts +45 -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/ChatPage.tsx +10 -324
  36. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +80 -0
  37. package/src/components/chat/adapters/chat-input-bar.adapter.ts +329 -0
  38. package/src/components/chat/adapters/chat-message.adapter.test.ts +138 -0
  39. package/src/components/chat/adapters/chat-message.adapter.ts +200 -0
  40. package/src/components/chat/chat-chain.test.ts +22 -0
  41. package/src/components/chat/chat-chain.ts +23 -0
  42. package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +128 -0
  43. package/src/components/chat/chat-input/chat-input-bar.controller.ts +105 -0
  44. package/src/components/chat/chat-page-shell.tsx +103 -0
  45. package/src/components/chat/containers/chat-input-bar.container.tsx +270 -0
  46. package/src/components/chat/containers/chat-message-list.container.tsx +71 -0
  47. package/src/components/chat/index.ts +1 -0
  48. package/src/components/chat/legacy/LegacyChatPage.tsx +228 -0
  49. package/src/components/chat/managers/chat-thread.manager.ts +3 -1
  50. package/src/components/chat/ncp/NcpChatPage.tsx +349 -0
  51. package/src/components/chat/ncp/ncp-chat-input.manager.ts +173 -0
  52. package/src/components/chat/ncp/ncp-chat-page-data.ts +134 -0
  53. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +89 -0
  54. package/src/components/chat/ncp/ncp-chat.presenter.ts +33 -0
  55. package/src/components/chat/ncp/ncp-session-adapter.test.ts +49 -0
  56. package/src/components/chat/ncp/ncp-session-adapter.ts +194 -0
  57. package/src/components/chat/nextclaw/index.ts +23 -0
  58. package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
  59. package/src/components/config/runtime-security-card.tsx +276 -0
  60. package/src/components/config/security-config.tsx +12 -0
  61. package/src/components/layout/Sidebar.tsx +6 -1
  62. package/src/components/marketplace/MarketplacePage.test.tsx +170 -0
  63. package/src/components/marketplace/MarketplacePage.tsx +77 -28
  64. package/src/hooks/use-auth.ts +111 -0
  65. package/src/hooks/useConfig.ts +42 -0
  66. package/src/hooks/useMarketplace.ts +9 -0
  67. package/src/lib/i18n.ts +73 -1
  68. package/src/test/setup.ts +16 -0
  69. package/tailwind.config.js +8 -3
  70. package/tsconfig.json +6 -2
  71. package/vite.config.ts +2 -1
  72. package/vitest.config.ts +16 -0
  73. package/dist/assets/ChannelsList-DzeVn-JC.js +0 -1
  74. package/dist/assets/ChatPage-BiFhIm1-.js +0 -36
  75. package/dist/assets/DocBrowser-By3lF9yN.js +0 -1
  76. package/dist/assets/MarketplacePage-EZxALdIz.js +0 -49
  77. package/dist/assets/ModelConfig-AchYxLft.js +0 -1
  78. package/dist/assets/ProvidersList-BsD-4kKX.js +0 -1
  79. package/dist/assets/RuntimeConfig-sKOERbFD.js +0 -1
  80. package/dist/assets/SearchConfig-DAfvDwX6.js +0 -1
  81. package/dist/assets/SessionsConfig-CzvrKDRs.js +0 -2
  82. package/dist/assets/card-BAM7vbMg.js +0 -1
  83. package/dist/assets/index-D9rRqOi8.css +0 -1
  84. package/dist/assets/index-DJZ5y7t1.js +0 -8
  85. package/dist/assets/input-BoelTiYL.js +0 -1
  86. package/dist/assets/page-layout-CERNdqzB.js +0 -1
  87. package/dist/assets/popover-uwYz3Chm.js +0 -1
  88. package/dist/assets/tabs-custom-pDyl95el.js +0 -1
  89. package/dist/assets/useConfirmDialog-DyP6Ac75.js +0 -5
  90. package/dist/assets/vendor-BKtTvQYU.js +0 -407
  91. package/src/components/chat/ChatThread.tsx +0 -402
  92. package/src/components/chat/SkillsPicker.tsx +0 -137
  93. package/src/components/chat/chat-input/ChatInputBarView.tsx +0 -82
  94. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +0 -83
  95. package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +0 -39
  96. package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +0 -31
  97. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +0 -112
  98. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +0 -24
  99. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +0 -58
  100. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +0 -56
  101. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +0 -40
  102. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +0 -74
  103. package/src/components/chat/chat-input/useChatInputBarController.ts +0 -322
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-DJZ5y7t1.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-BKtTvQYU.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-D9rRqOi8.css">
9
+ <script type="module" crossorigin src="/assets/index-CzkY1reu.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-psXJBy9u.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-RZ0kHHRI.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.6.15",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,34 +20,41 @@
20
20
  "react": "^18.3.1",
21
21
  "react-dom": "^18.3.1",
22
22
  "react-hook-form": "^7.53.2",
23
- "react-markdown": "^10.1.0",
24
23
  "react-router-dom": "^7.13.0",
25
24
  "rehype-sanitize": "^6.0.0",
26
- "remark-gfm": "^4.0.1",
27
25
  "rxjs": "^7.8.2",
28
26
  "sonner": "^1.7.1",
29
27
  "tailwind-merge": "^2.5.4",
30
28
  "zod": "^3.23.8",
31
29
  "zustand": "^5.0.2",
30
+ "@nextclaw/ncp-http-agent-client": "0.3.0",
31
+ "@nextclaw/ncp-react": "0.3.0",
32
+ "@nextclaw/agent-chat-ui": "0.2.0",
33
+ "@nextclaw/ncp": "0.3.0",
32
34
  "@nextclaw/agent-chat": "0.1.1"
33
35
  },
34
36
  "devDependencies": {
37
+ "@testing-library/react": "^16.3.0",
38
+ "@testing-library/user-event": "^14.6.1",
35
39
  "@types/react": "^18.3.12",
36
40
  "@types/react-dom": "^18.3.1",
37
41
  "@vitejs/plugin-react": "^4.3.4",
38
42
  "autoprefixer": "^10.4.20",
43
+ "jsdom": "^25.0.1",
39
44
  "postcss": "^8.4.49",
40
45
  "prettier": "^3.3.3",
41
46
  "tailwindcss": "^3.4.15",
42
47
  "tailwindcss-animate": "^1.0.7",
43
48
  "typescript": "^5.6.3",
44
- "vite": "^6.0.1"
49
+ "vite": "^6.0.1",
50
+ "vitest": "^2.1.2"
45
51
  },
46
52
  "scripts": {
47
53
  "dev": "vite",
48
54
  "build": "tsc && vite build",
49
55
  "preview": "vite preview",
50
56
  "lint": "eslint .",
51
- "tsc": "tsc --noEmit"
57
+ "tsc": "tsc --noEmit",
58
+ "test": "vitest run"
52
59
  }
53
60
  }
package/src/App.tsx CHANGED
@@ -1,6 +1,8 @@
1
1
  import { lazy, Suspense } from 'react';
2
2
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
+ import { LoginPage } from '@/components/auth/login-page';
3
4
  import { AppLayout } from '@/components/layout/AppLayout';
5
+ import { useAuthStatus } from '@/hooks/use-auth';
4
6
  import { useWebSocket } from '@/hooks/useWebSocket';
5
7
  import { Toaster } from 'sonner';
6
8
  import { Routes, Route, Navigate } from 'react-router-dom';
@@ -20,6 +22,7 @@ const SearchConfigPage = lazy(async () => ({ default: (await import('@/component
20
22
  const ProvidersListPage = lazy(async () => ({ default: (await import('@/components/config/ProvidersList')).ProvidersList }));
21
23
  const ChannelsListPage = lazy(async () => ({ default: (await import('@/components/config/ChannelsList')).ChannelsList }));
22
24
  const RuntimeConfigPage = lazy(async () => ({ default: (await import('@/components/config/RuntimeConfig')).RuntimeConfig }));
25
+ const SecurityConfigPage = lazy(async () => ({ default: (await import('@/components/config/security-config')).SecurityConfig }));
23
26
  const SessionsConfigPage = lazy(async () => ({ default: (await import('@/components/config/SessionsConfig')).SessionsConfig }));
24
27
  const SecretsConfigPage = lazy(async () => ({ default: (await import('@/components/config/SecretsConfig')).SecretsConfig }));
25
28
  const MarketplacePage = lazy(async () => ({ default: (await import('@/components/marketplace/MarketplacePage')).MarketplacePage }));
@@ -32,38 +35,57 @@ function LazyRoute({ children }: { children: JSX.Element }) {
32
35
  return <Suspense fallback={<RouteFallback />}>{children}</Suspense>;
33
36
  }
34
37
 
35
- function AppContent() {
38
+ function ProtectedApp() {
36
39
  useWebSocket(queryClient); // Initialize WebSocket connection
37
40
 
41
+ return (
42
+ <AppLayout>
43
+ <div className="w-full h-full">
44
+ <Routes>
45
+ <Route path="/chat/skills" element={<Navigate to="/skills" replace />} />
46
+ <Route path="/chat/cron" element={<Navigate to="/cron" replace />} />
47
+ <Route path="/chat/:sessionId?" element={<LazyRoute><ChatPage view="chat" /></LazyRoute>} />
48
+ <Route path="/skills" element={<LazyRoute><ChatPage view="skills" /></LazyRoute>} />
49
+ <Route path="/cron" element={<LazyRoute><ChatPage view="cron" /></LazyRoute>} />
50
+ <Route path="/model" element={<LazyRoute><ModelConfigPage /></LazyRoute>} />
51
+ <Route path="/search" element={<LazyRoute><SearchConfigPage /></LazyRoute>} />
52
+ <Route path="/providers" element={<LazyRoute><ProvidersListPage /></LazyRoute>} />
53
+ <Route path="/channels" element={<LazyRoute><ChannelsListPage /></LazyRoute>} />
54
+ <Route path="/runtime" element={<LazyRoute><RuntimeConfigPage /></LazyRoute>} />
55
+ <Route path="/security" element={<LazyRoute><SecurityConfigPage /></LazyRoute>} />
56
+ <Route path="/sessions" element={<LazyRoute><SessionsConfigPage /></LazyRoute>} />
57
+ <Route path="/secrets" element={<LazyRoute><SecretsConfigPage /></LazyRoute>} />
58
+ <Route path="/settings" element={<Navigate to="/model" replace />} />
59
+ <Route path="/marketplace/skills" element={<Navigate to="/skills" replace />} />
60
+ <Route path="/marketplace" element={<Navigate to="/marketplace/plugins" replace />} />
61
+ <Route path="/marketplace/:type" element={<LazyRoute><MarketplacePage /></LazyRoute>} />
62
+ <Route path="/" element={<Navigate to="/chat" replace />} />
63
+ <Route path="*" element={<Navigate to="/chat" replace />} />
64
+ </Routes>
65
+ </div>
66
+ </AppLayout>
67
+ );
68
+ }
69
+
70
+ function AuthGate() {
71
+ const authStatus = useAuthStatus();
72
+
73
+ if (authStatus.isLoading && !authStatus.isError) {
74
+ return <RouteFallback />;
75
+ }
76
+
77
+ if (authStatus.data?.enabled && !authStatus.data.authenticated) {
78
+ return <LoginPage username={authStatus.data.username} />;
79
+ }
80
+
81
+ return <ProtectedApp />;
82
+ }
83
+
84
+ export default function AppContent() {
38
85
  return (
39
86
  <QueryClientProvider client={queryClient}>
40
- <AppLayout>
41
- <div className="w-full h-full">
42
- <Routes>
43
- <Route path="/chat/skills" element={<Navigate to="/skills" replace />} />
44
- <Route path="/chat/cron" element={<Navigate to="/cron" replace />} />
45
- <Route path="/chat/:sessionId?" element={<LazyRoute><ChatPage view="chat" /></LazyRoute>} />
46
- <Route path="/skills" element={<LazyRoute><ChatPage view="skills" /></LazyRoute>} />
47
- <Route path="/cron" element={<LazyRoute><ChatPage view="cron" /></LazyRoute>} />
48
- <Route path="/model" element={<LazyRoute><ModelConfigPage /></LazyRoute>} />
49
- <Route path="/search" element={<LazyRoute><SearchConfigPage /></LazyRoute>} />
50
- <Route path="/providers" element={<LazyRoute><ProvidersListPage /></LazyRoute>} />
51
- <Route path="/channels" element={<LazyRoute><ChannelsListPage /></LazyRoute>} />
52
- <Route path="/runtime" element={<LazyRoute><RuntimeConfigPage /></LazyRoute>} />
53
- <Route path="/sessions" element={<LazyRoute><SessionsConfigPage /></LazyRoute>} />
54
- <Route path="/secrets" element={<LazyRoute><SecretsConfigPage /></LazyRoute>} />
55
- <Route path="/settings" element={<Navigate to="/model" replace />} />
56
- <Route path="/marketplace/skills" element={<Navigate to="/skills" replace />} />
57
- <Route path="/marketplace" element={<Navigate to="/marketplace/plugins" replace />} />
58
- <Route path="/marketplace/:type" element={<LazyRoute><MarketplacePage /></LazyRoute>} />
59
- <Route path="/" element={<Navigate to="/chat" replace />} />
60
- <Route path="*" element={<Navigate to="/chat" replace />} />
61
- </Routes>
62
- </div>
63
- </AppLayout>
87
+ <AuthGate />
64
88
  <Toaster position="top-right" richColors />
65
89
  </QueryClientProvider>
66
90
  );
67
91
  }
68
-
69
- export default AppContent;
package/src/api/client.ts CHANGED
@@ -50,6 +50,7 @@ async function apiRequest<T>(
50
50
  const method = (options.method || 'GET').toUpperCase();
51
51
 
52
52
  const response = await fetch(url, {
53
+ credentials: 'include',
53
54
  headers: {
54
55
  'Content-Type': 'application/json',
55
56
  ...options.headers
package/src/api/config.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { api, API_BASE } from './client';
2
2
  import type {
3
+ AuthEnabledUpdateRequest,
4
+ AuthLoginRequest,
5
+ AuthPasswordUpdateRequest,
6
+ AuthSetupRequest,
7
+ AuthStatusView,
3
8
  AppMetaView,
4
9
  ConfigView,
5
10
  ConfigMetaView,
@@ -19,6 +24,8 @@ import type {
19
24
  ProviderCreateRequest,
20
25
  ProviderCreateResult,
21
26
  ProviderDeleteResult,
27
+ NcpSessionMessagesView,
28
+ NcpSessionsListView,
22
29
  RuntimeConfigUpdate,
23
30
  SecretsConfigUpdate,
24
31
  SecretsView,
@@ -46,6 +53,60 @@ import type {
46
53
  CronActionResult
47
54
  } from './types';
48
55
 
56
+ // GET /api/auth/status
57
+ export async function fetchAuthStatus(): Promise<AuthStatusView> {
58
+ const response = await api.get<AuthStatusView>('/api/auth/status');
59
+ if (!response.ok) {
60
+ throw new Error(response.error.message);
61
+ }
62
+ return response.data;
63
+ }
64
+
65
+ // POST /api/auth/setup
66
+ export async function setupAuth(data: AuthSetupRequest): Promise<AuthStatusView> {
67
+ const response = await api.post<AuthStatusView>('/api/auth/setup', data);
68
+ if (!response.ok) {
69
+ throw new Error(response.error.message);
70
+ }
71
+ return response.data;
72
+ }
73
+
74
+ // POST /api/auth/login
75
+ export async function loginAuth(data: AuthLoginRequest): Promise<AuthStatusView> {
76
+ const response = await api.post<AuthStatusView>('/api/auth/login', data);
77
+ if (!response.ok) {
78
+ throw new Error(response.error.message);
79
+ }
80
+ return response.data;
81
+ }
82
+
83
+ // POST /api/auth/logout
84
+ export async function logoutAuth(): Promise<{ success: boolean }> {
85
+ const response = await api.post<{ success: boolean }>('/api/auth/logout', {});
86
+ if (!response.ok) {
87
+ throw new Error(response.error.message);
88
+ }
89
+ return response.data;
90
+ }
91
+
92
+ // PUT /api/auth/password
93
+ export async function updateAuthPassword(data: AuthPasswordUpdateRequest): Promise<AuthStatusView> {
94
+ const response = await api.put<AuthStatusView>('/api/auth/password', data);
95
+ if (!response.ok) {
96
+ throw new Error(response.error.message);
97
+ }
98
+ return response.data;
99
+ }
100
+
101
+ // PUT /api/auth/enabled
102
+ export async function updateAuthEnabled(data: AuthEnabledUpdateRequest): Promise<AuthStatusView> {
103
+ const response = await api.put<AuthStatusView>('/api/auth/enabled', data);
104
+ if (!response.ok) {
105
+ throw new Error(response.error.message);
106
+ }
107
+ return response.data;
108
+ }
109
+
49
110
  // GET /api/app/meta
50
111
  export async function fetchAppMeta(): Promise<AppMetaView> {
51
112
  const response = await api.get<AppMetaView>('/api/app/meta');
@@ -310,6 +371,42 @@ export async function deleteSession(key: string): Promise<{ deleted: boolean }>
310
371
  return response.data;
311
372
  }
312
373
 
374
+ // GET /api/ncp/sessions
375
+ export async function fetchNcpSessions(params?: { limit?: number }): Promise<NcpSessionsListView> {
376
+ const query = new URLSearchParams();
377
+ if (typeof params?.limit === 'number' && Number.isFinite(params.limit)) {
378
+ query.set('limit', String(Math.max(1, Math.trunc(params.limit))));
379
+ }
380
+ const suffix = query.toString();
381
+ const response = await api.get<NcpSessionsListView>(suffix ? `/api/ncp/sessions?${suffix}` : '/api/ncp/sessions');
382
+ if (!response.ok) {
383
+ throw new Error(response.error.message);
384
+ }
385
+ return response.data;
386
+ }
387
+
388
+ // GET /api/ncp/sessions/:sessionId/messages
389
+ export async function fetchNcpSessionMessages(sessionId: string, limit = 200): Promise<NcpSessionMessagesView> {
390
+ const response = await api.get<NcpSessionMessagesView>(
391
+ `/api/ncp/sessions/${encodeURIComponent(sessionId)}/messages?limit=${Math.max(1, Math.trunc(limit))}`
392
+ );
393
+ if (!response.ok) {
394
+ throw new Error(response.error.message);
395
+ }
396
+ return response.data;
397
+ }
398
+
399
+ // DELETE /api/ncp/sessions/:sessionId
400
+ export async function deleteNcpSession(sessionId: string): Promise<{ deleted: boolean; sessionId: string }> {
401
+ const response = await api.delete<{ deleted: boolean; sessionId: string }>(
402
+ `/api/ncp/sessions/${encodeURIComponent(sessionId)}`
403
+ );
404
+ if (!response.ok) {
405
+ throw new Error(response.error.message);
406
+ }
407
+ return response.data;
408
+ }
409
+
313
410
  // POST /api/chat/turn
314
411
  export async function sendChatTurn(data: ChatTurnRequest): Promise<ChatTurnView> {
315
412
  const response = await api.post<ChatTurnView>('/api/chat/turn', data);
@@ -356,6 +453,7 @@ async function readSseStream(params: {
356
453
  }): Promise<{ sessionKey: string; reply: string }> {
357
454
  const response = await fetch(`${API_BASE}${params.path}`, {
358
455
  method: params.method,
456
+ credentials: 'include',
359
457
  headers: {
360
458
  'Content-Type': 'application/json',
361
459
  Accept: 'text/event-stream'
package/src/api/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { NcpMessage, NcpSessionStatus, NcpSessionSummary } from '@nextclaw/ncp';
2
+
1
3
  // API Types - matching backend response format
2
4
  export type ApiError = {
3
5
  code: string;
@@ -158,6 +160,31 @@ export type ProviderAuthImportResult = {
158
160
  expiresAt?: string;
159
161
  };
160
162
 
163
+ export type AuthStatusView = {
164
+ enabled: boolean;
165
+ configured: boolean;
166
+ authenticated: boolean;
167
+ username?: string;
168
+ };
169
+
170
+ export type AuthSetupRequest = {
171
+ username: string;
172
+ password: string;
173
+ };
174
+
175
+ export type AuthLoginRequest = {
176
+ username: string;
177
+ password: string;
178
+ };
179
+
180
+ export type AuthPasswordUpdateRequest = {
181
+ password: string;
182
+ };
183
+
184
+ export type AuthEnabledUpdateRequest = {
185
+ enabled: boolean;
186
+ };
187
+
161
188
  export type AgentProfileView = {
162
189
  id: string;
163
190
  default?: boolean;
@@ -236,6 +263,23 @@ export type SessionHistoryView = {
236
263
  events: SessionEventView[];
237
264
  };
238
265
 
266
+ export type NcpSessionSummaryView = NcpSessionSummary;
267
+
268
+ export type NcpSessionsListView = {
269
+ sessions: NcpSessionSummaryView[];
270
+ total: number;
271
+ };
272
+
273
+ export type NcpMessageView = NcpMessage;
274
+
275
+ export type NcpSessionMessagesView = {
276
+ sessionId: string;
277
+ messages: NcpMessageView[];
278
+ total: number;
279
+ };
280
+
281
+ export type NcpSessionStatusView = NcpSessionStatus;
282
+
239
283
  export type SessionPatchUpdate = {
240
284
  label?: string | null;
241
285
  preferredModel?: string | null;
@@ -501,6 +545,7 @@ export type ConfigView = {
501
545
  session?: SessionConfigView;
502
546
  tools?: Record<string, unknown>;
503
547
  gateway?: Record<string, unknown>;
548
+ ui?: Record<string, unknown>;
504
549
  secrets?: SecretsView;
505
550
  };
506
551
 
@@ -0,0 +1,69 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4
+ import { Input } from '@/components/ui/input';
5
+ import { useLoginAuth } from '@/hooks/use-auth';
6
+ import { t } from '@/lib/i18n';
7
+
8
+ type LoginPageProps = {
9
+ username?: string;
10
+ };
11
+
12
+ export function LoginPage({ username }: LoginPageProps) {
13
+ const loginMutation = useLoginAuth();
14
+ const [formUsername, setFormUsername] = useState(username ?? '');
15
+ const [password, setPassword] = useState('');
16
+ const canSubmit = formUsername.trim().length > 0 && password.length > 0 && !loginMutation.isPending;
17
+
18
+ const handleSubmit = (event?: FormEvent<HTMLFormElement>) => {
19
+ event?.preventDefault();
20
+ if (!canSubmit) {
21
+ return;
22
+ }
23
+ loginMutation.mutate({
24
+ username: formUsername.trim(),
25
+ password
26
+ });
27
+ };
28
+
29
+ return (
30
+ <main className="flex min-h-screen items-center justify-center bg-secondary px-6 py-10">
31
+ <Card hover={false} className="w-full max-w-md shadow-card-hover">
32
+ <CardHeader>
33
+ <p className="text-xs font-semibold uppercase tracking-[0.24em] text-gray-500">{t('authBrand')}</p>
34
+ <CardTitle className="text-2xl">{t('authLoginTitle')}</CardTitle>
35
+ <CardDescription>{t('authLoginDescription')}</CardDescription>
36
+ </CardHeader>
37
+ <CardContent>
38
+ <form className="space-y-4" onSubmit={handleSubmit}>
39
+ <div className="space-y-2">
40
+ <label className="text-sm font-medium text-gray-800">{t('authUsername')}</label>
41
+ <Input
42
+ value={formUsername}
43
+ onChange={(event) => setFormUsername(event.target.value)}
44
+ placeholder={t('authUsernamePlaceholder')}
45
+ autoFocus
46
+ />
47
+ </div>
48
+ <div className="space-y-2">
49
+ <label className="text-sm font-medium text-gray-800">{t('authPassword')}</label>
50
+ <Input
51
+ type="password"
52
+ value={password}
53
+ onChange={(event) => setPassword(event.target.value)}
54
+ placeholder={t('authPasswordPlaceholder')}
55
+ />
56
+ </div>
57
+ <Button
58
+ type="submit"
59
+ className="w-full"
60
+ disabled={!canSubmit}
61
+ >
62
+ {loginMutation.isPending ? t('authLoggingIn') : t('authLoginAction')}
63
+ </Button>
64
+ </form>
65
+ </CardContent>
66
+ </Card>
67
+ </main>
68
+ );
69
+ }
@@ -1,16 +1,14 @@
1
- import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
1
+ import { useRef } from 'react';
2
+ import { useStickyBottomScroll } from '@nextclaw/agent-chat-ui';
2
3
  import { Button } from '@/components/ui/button';
3
- import { ChatThread } from '@/components/chat/ChatThread';
4
+ import { ChatInputBarContainer, ChatMessageListContainer } from '@/components/chat/nextclaw';
4
5
  import { ChatWelcome } from '@/components/chat/ChatWelcome';
5
- import { ChatInputBarView } from '@/components/chat/chat-input/ChatInputBarView';
6
6
  import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
7
7
  import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
8
8
  import { t } from '@/lib/i18n';
9
9
  import { cn } from '@/lib/utils';
10
10
  import { Trash2 } from 'lucide-react';
11
11
 
12
- const STICKY_BOTTOM_THRESHOLD_PX = 10;
13
-
14
12
  function ChatConversationSkeleton() {
15
13
  return (
16
14
  <section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
@@ -45,12 +43,6 @@ export function ChatConversationPanel() {
45
43
  const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
46
44
  const threadRef = snapshot.threadRef ?? fallbackThreadRef;
47
45
 
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
46
  const showWelcome = !snapshot.selectedSessionKey && snapshot.uiMessages.length === 0 && !snapshot.isSending;
55
47
  const hasConfiguredModel = snapshot.modelOptions.length > 0;
56
48
  const shouldShowProviderHint = snapshot.isProviderStateResolved && !hasConfiguredModel;
@@ -60,47 +52,13 @@ export function ChatConversationPanel() {
60
52
  !snapshot.isSending &&
61
53
  !snapshot.isAwaitingAssistantOutput;
62
54
 
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]);
55
+ const { onScroll: handleScroll } = useStickyBottomScroll({
56
+ scrollRef: threadRef,
57
+ resetKey: snapshot.selectedSessionKey,
58
+ isLoading: snapshot.isHistoryLoading,
59
+ hasContent: snapshot.uiMessages.length > 0,
60
+ contentVersion: snapshot.uiMessages
61
+ });
104
62
 
105
63
  if (!snapshot.isProviderStateResolved) {
106
64
  return <ChatConversationSkeleton />;
@@ -160,12 +118,12 @@ export function ChatConversationPanel() {
160
118
  <div className="px-5 py-5 text-sm text-gray-500">{t('chatNoMessages')}</div>
161
119
  ) : (
162
120
  <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
163
- <ChatThread uiMessages={snapshot.uiMessages} isSending={snapshot.isSending && snapshot.isAwaitingAssistantOutput} />
121
+ <ChatMessageListContainer uiMessages={snapshot.uiMessages} isSending={snapshot.isSending && snapshot.isAwaitingAssistantOutput} />
164
122
  </div>
165
123
  )}
166
124
  </div>
167
125
 
168
- <ChatInputBarView />
126
+ <ChatInputBarContainer />
169
127
  </section>
170
128
  );
171
129
  }