@nextclaw/ui 0.5.48 → 0.6.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 (57) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/assets/ChannelsList-CkCpHSto.js +1 -0
  3. package/dist/assets/ChatPage-DM4XNsrW.js +32 -0
  4. package/dist/assets/DocBrowser-B5Aqiz6W.js +1 -0
  5. package/dist/assets/MarketplacePage-BIi0bBdW.js +49 -0
  6. package/dist/assets/ModelConfig-BTFiEAxQ.js +1 -0
  7. package/dist/assets/{ProvidersList-BXHpjVtO.js → ProvidersList-cdk1d-G_.js} +1 -1
  8. package/dist/assets/RuntimeConfig-CFqFsXmR.js +1 -0
  9. package/dist/assets/{SecretsConfig-KkgMzdt1.js → SecretsConfig-CIKasCek.js} +2 -2
  10. package/dist/assets/SessionsConfig-mnCLFtbo.js +2 -0
  11. package/dist/assets/{card-D7NY0Szf.js → card-C1BUfR85.js} +1 -1
  12. package/dist/assets/index-Dxas8MJ9.js +2 -0
  13. package/dist/assets/index-P4YzN9iS.css +1 -0
  14. package/dist/assets/{label-Ojs7Al6B.js → label-CwWfYbuj.js} +1 -1
  15. package/dist/assets/{logos-B1qBsCSi.js → logos-DDyjHSEU.js} +1 -1
  16. package/dist/assets/{page-layout-CUMMO0nN.js → page-layout-DKTRKcHL.js} +1 -1
  17. package/dist/assets/provider-models-y4mUDcGF.js +1 -0
  18. package/dist/assets/{switch-BdhS_16-.js → switch-Bi3yeYiC.js} +1 -1
  19. package/dist/assets/{tabs-custom-D261E5EA.js → tabs-custom-HZFNZrc0.js} +1 -1
  20. package/dist/assets/useConfig-CgzVQTZl.js +6 -0
  21. package/dist/assets/{useConfirmDialog-BUKGHDL6.js → useConfirmDialog-DwD21HlD.js} +2 -2
  22. package/dist/assets/{vendor-Dh04PGww.js → vendor-Ylg6Wdt_.js} +84 -69
  23. package/dist/index.html +3 -3
  24. package/package.json +2 -1
  25. package/src/App.tsx +10 -6
  26. package/src/api/config.ts +42 -1
  27. package/src/api/types.ts +29 -0
  28. package/src/components/chat/ChatConversationPanel.tsx +109 -85
  29. package/src/components/chat/ChatInputBar.tsx +245 -0
  30. package/src/components/chat/ChatPage.tsx +365 -187
  31. package/src/components/chat/ChatSidebar.tsx +242 -0
  32. package/src/components/chat/ChatThread.tsx +92 -25
  33. package/src/components/chat/ChatWelcome.tsx +61 -0
  34. package/src/components/chat/SkillsPicker.tsx +137 -0
  35. package/src/components/chat/useChatStreamController.ts +287 -56
  36. package/src/components/config/ChannelForm.tsx +1 -1
  37. package/src/components/config/ChannelsList.tsx +3 -3
  38. package/src/components/config/ModelConfig.tsx +11 -89
  39. package/src/components/config/RuntimeConfig.tsx +29 -1
  40. package/src/components/layout/AppLayout.tsx +42 -6
  41. package/src/components/layout/Sidebar.tsx +68 -62
  42. package/src/components/marketplace/MarketplacePage.tsx +13 -3
  43. package/src/components/ui/popover.tsx +31 -0
  44. package/src/hooks/useConfig.ts +18 -0
  45. package/src/lib/i18n.ts +53 -0
  46. package/src/lib/provider-models.ts +129 -0
  47. package/dist/assets/ChannelsList-C8cguFLc.js +0 -1
  48. package/dist/assets/ChatPage-BkHWNUNR.js +0 -32
  49. package/dist/assets/CronConfig-D-ESQlvk.js +0 -1
  50. package/dist/assets/DocBrowser-B9ZD6pAk.js +0 -1
  51. package/dist/assets/MarketplacePage-Ds_l9KTF.js +0 -49
  52. package/dist/assets/ModelConfig-N1tbLv9b.js +0 -1
  53. package/dist/assets/RuntimeConfig-KsKfkjgv.js +0 -1
  54. package/dist/assets/SessionsConfig-CWBp8IPf.js +0 -2
  55. package/dist/assets/index-BRBYYgR_.js +0 -2
  56. package/dist/assets/index-C5cdRzpO.css +0 -1
  57. package/dist/assets/useConfig-txxbxXnT.js +0 -6
@@ -28,6 +28,7 @@ function createEmptyAgent(): AgentProfileView {
28
28
  default: false,
29
29
  workspace: '',
30
30
  model: '',
31
+ engine: '',
31
32
  contextTokens: undefined,
32
33
  maxToolIterations: undefined
33
34
  };
@@ -62,6 +63,7 @@ export function RuntimeConfig() {
62
63
  const [dmScope, setDmScope] = useState<DmScope>('per-channel-peer');
63
64
  const [maxPingPongTurns, setMaxPingPongTurns] = useState(0);
64
65
  const [defaultContextTokens, setDefaultContextTokens] = useState(200000);
66
+ const [defaultEngine, setDefaultEngine] = useState('native');
65
67
 
66
68
  useEffect(() => {
67
69
  if (!config) {
@@ -73,6 +75,7 @@ export function RuntimeConfig() {
73
75
  default: Boolean(agent.default),
74
76
  workspace: agent.workspace ?? '',
75
77
  model: agent.model ?? '',
78
+ engine: agent.engine ?? '',
76
79
  contextTokens: agent.contextTokens,
77
80
  maxToolIterations: agent.maxToolIterations
78
81
  }))
@@ -95,13 +98,16 @@ export function RuntimeConfig() {
95
98
  setDmScope((config.session?.dmScope as DmScope) ?? 'per-channel-peer');
96
99
  setMaxPingPongTurns(config.session?.agentToAgent?.maxPingPongTurns ?? 0);
97
100
  setDefaultContextTokens(config.agents.defaults.contextTokens ?? 200000);
101
+ setDefaultEngine(config.agents.defaults.engine ?? 'native');
98
102
  }, [config]);
99
103
 
100
104
  const uiHints = schema?.uiHints;
101
105
  const dmScopeHint = hintForPath('session.dmScope', uiHints);
102
106
  const maxPingHint = hintForPath('session.agentToAgent.maxPingPongTurns', uiHints);
103
107
  const defaultContextTokensHint = hintForPath('agents.defaults.contextTokens', uiHints);
108
+ const defaultEngineHint = hintForPath('agents.defaults.engine', uiHints);
104
109
  const agentContextTokensHint = hintForPath('agents.list.*.contextTokens', uiHints);
110
+ const agentEngineHint = hintForPath('agents.list.*.engine', uiHints);
105
111
  const agentsHint = hintForPath('agents.list', uiHints);
106
112
  const bindingsHint = hintForPath('bindings', uiHints);
107
113
 
@@ -141,6 +147,9 @@ export function RuntimeConfig() {
141
147
  if (agent.model?.trim()) {
142
148
  normalized.model = agent.model.trim();
143
149
  }
150
+ if (agent.engine?.trim()) {
151
+ normalized.engine = agent.engine.trim();
152
+ }
144
153
  if (typeof agent.contextTokens === 'number') {
145
154
  normalized.contextTokens = Math.max(1000, agent.contextTokens);
146
155
  }
@@ -203,7 +212,8 @@ export function RuntimeConfig() {
203
212
  data: {
204
213
  agents: {
205
214
  defaults: {
206
- contextTokens: Math.max(1000, defaultContextTokens)
215
+ contextTokens: Math.max(1000, defaultContextTokens),
216
+ engine: defaultEngine.trim() || 'native'
207
217
  },
208
218
  list: normalizedAgents
209
219
  },
@@ -251,6 +261,19 @@ export function RuntimeConfig() {
251
261
  {defaultContextTokensHint?.help ?? t('defaultContextTokensHelp')}
252
262
  </p>
253
263
  </div>
264
+ <div className="space-y-2">
265
+ <label className="text-sm font-medium text-gray-800">
266
+ {defaultEngineHint?.label ?? t('defaultEngine')}
267
+ </label>
268
+ <Input
269
+ value={defaultEngine}
270
+ onChange={(event) => setDefaultEngine(event.target.value)}
271
+ placeholder={t('defaultEnginePlaceholder')}
272
+ />
273
+ <p className="text-xs text-gray-500">
274
+ {defaultEngineHint?.help ?? t('defaultEngineHelp')}
275
+ </p>
276
+ </div>
254
277
  <div className="space-y-2">
255
278
  <label className="text-sm font-medium text-gray-800">{dmScopeHint?.label ?? t('dmScope')}</label>
256
279
  <Select value={dmScope} onValueChange={(v) => setDmScope(v as DmScope)}>
@@ -308,6 +331,11 @@ export function RuntimeConfig() {
308
331
  onChange={(event) => updateAgent(index, { model: event.target.value })}
309
332
  placeholder={t('modelOverridePlaceholder')}
310
333
  />
334
+ <Input
335
+ value={agent.engine ?? ''}
336
+ onChange={(event) => updateAgent(index, { engine: event.target.value })}
337
+ placeholder={agentEngineHint?.label ?? t('engineOverridePlaceholder')}
338
+ />
311
339
  <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
312
340
  <Input
313
341
  type="number"
@@ -1,7 +1,9 @@
1
1
  import { lazy, Suspense } from 'react';
2
+ import { useLocation } from 'react-router-dom';
2
3
  import { Sidebar } from './Sidebar';
3
4
  import { DocBrowserProvider, useDocBrowser } from '@/components/doc-browser/DocBrowserContext';
4
5
  import { useDocLinkInterceptor } from '@/components/doc-browser/useDocLinkInterceptor';
6
+ import { cn } from '@/lib/utils';
5
7
 
6
8
  const DocBrowser = lazy(async () => ({ default: (await import('@/components/doc-browser/DocBrowser')).DocBrowser }));
7
9
 
@@ -9,20 +11,54 @@ interface AppLayoutProps {
9
11
  children: React.ReactNode;
10
12
  }
11
13
 
14
+ function isMainWorkspaceRoute(pathname: string): boolean {
15
+ const normalized = pathname.toLowerCase();
16
+ return (
17
+ normalized === '/chat' ||
18
+ normalized.startsWith('/chat/') ||
19
+ normalized === '/skills' ||
20
+ normalized.startsWith('/skills/') ||
21
+ normalized === '/cron' ||
22
+ normalized.startsWith('/cron/')
23
+ );
24
+ }
25
+
26
+ function isChannelsRoute(pathname: string): boolean {
27
+ const normalized = pathname.toLowerCase();
28
+ return normalized === '/channels' || normalized.startsWith('/channels/');
29
+ }
30
+
12
31
  function AppLayoutInner({ children }: AppLayoutProps) {
13
32
  const { isOpen, mode } = useDocBrowser();
14
33
  useDocLinkInterceptor();
34
+ const { pathname } = useLocation();
35
+ const isMainRoute = isMainWorkspaceRoute(pathname);
36
+ const lockPageScroll = isChannelsRoute(pathname);
15
37
 
16
38
  return (
17
39
  <div className="h-screen flex bg-background font-sans text-foreground">
18
- <Sidebar />
40
+ {!isMainRoute && <Sidebar mode="settings" />}
19
41
  <div className="flex-1 flex min-w-0 overflow-hidden relative">
20
42
  <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
21
- <main className="flex-1 overflow-auto custom-scrollbar p-8">
22
- <div className="max-w-6xl mx-auto animate-fade-in h-full">
23
- {children}
24
- </div>
25
- </main>
43
+ {isMainRoute ? (
44
+ <div className="flex-1 h-full overflow-hidden">{children}</div>
45
+ ) : (
46
+ <main
47
+ className={cn(
48
+ 'flex-1 custom-scrollbar p-8',
49
+ lockPageScroll ? 'overflow-auto xl:overflow-hidden' : 'overflow-auto'
50
+ )}
51
+ >
52
+ <div
53
+ className={cn(
54
+ 'max-w-6xl mx-auto animate-fade-in h-full',
55
+ lockPageScroll && 'min-h-0 xl:overflow-hidden'
56
+ )}
57
+ >
58
+ {children}
59
+ </div>
60
+ </main>
61
+ )}
26
62
  </div>
27
63
  {/* Doc Browser: docked mode renders inline, floating mode renders as overlay */}
28
64
  {isOpen && mode === 'docked' && (
@@ -1,14 +1,20 @@
1
1
  import { cn } from '@/lib/utils';
2
2
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
3
3
  import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
4
- import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound } from 'lucide-react';
4
+ import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft } from 'lucide-react';
5
5
  import { NavLink } from 'react-router-dom';
6
6
  import { useDocBrowser } from '@/components/doc-browser';
7
7
  import { useI18n } from '@/components/providers/I18nProvider';
8
8
  import { useTheme } from '@/components/providers/ThemeProvider';
9
9
  import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
10
10
 
11
- export function Sidebar() {
11
+ type SidebarMode = 'main' | 'settings';
12
+
13
+ type SidebarProps = {
14
+ mode: SidebarMode;
15
+ };
16
+
17
+ export function Sidebar({ mode }: SidebarProps) {
12
18
  const docBrowser = useDocBrowser();
13
19
  const { language, setLanguage } = useI18n();
14
20
  const { theme, setTheme } = useTheme();
@@ -31,12 +37,25 @@ export function Sidebar() {
31
37
  };
32
38
 
33
39
  // Core navigation items - primary features
34
- const coreNavItems = [
40
+ const mainNavItems = [
35
41
  {
36
42
  target: '/chat',
37
43
  label: t('chat'),
38
44
  icon: MessageCircle,
39
45
  },
46
+ {
47
+ target: '/chat/cron',
48
+ label: t('cron'),
49
+ icon: AlarmClock,
50
+ },
51
+ {
52
+ target: '/chat/skills',
53
+ label: t('marketplaceFilterSkills'),
54
+ icon: BrainCircuit,
55
+ }
56
+ ];
57
+
58
+ const settingsNavItems = [
40
59
  {
41
60
  target: '/model',
42
61
  label: t('model'),
@@ -52,20 +71,6 @@ export function Sidebar() {
52
71
  label: t('channels'),
53
72
  icon: MessageSquare,
54
73
  },
55
- {
56
- target: '/cron',
57
- label: t('cron'),
58
- icon: AlarmClock,
59
- },
60
- {
61
- target: '/marketplace/skills',
62
- label: t('marketplaceFilterSkills'),
63
- icon: BrainCircuit,
64
- }
65
- ];
66
-
67
- // Advanced navigation items - secondary features
68
- const advancedNavItems = [
69
74
  {
70
75
  target: '/runtime',
71
76
  label: t('runtime'),
@@ -87,24 +92,41 @@ export function Sidebar() {
87
92
  icon: Plug,
88
93
  }
89
94
  ];
95
+ const navItems = mode === 'main' ? mainNavItems : settingsNavItems;
90
96
 
91
97
  return (
92
98
  <aside className="w-[240px] shrink-0 flex flex-col h-full py-6 px-4 bg-secondary">
93
- {/* Logo Area */}
94
- <div className="px-2 mb-8">
95
- <div className="flex items-center gap-2.5 cursor-pointer">
96
- <div className="h-7 w-7 rounded-lg overflow-hidden flex items-center justify-center">
97
- <img src="/logo.svg" alt="NextClaw" className="h-full w-full object-contain" />
99
+ {mode === 'settings' ? (
100
+ <div className="px-2 mb-6">
101
+ <NavLink
102
+ to="/chat"
103
+ className="group inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-50 hover:text-gray-900"
104
+ >
105
+ <ArrowLeft className="h-3.5 w-3.5 text-gray-500 group-hover:text-gray-800" />
106
+ <span>{t('backToMain')}</span>
107
+ </NavLink>
108
+ <div className="mt-5 px-1">
109
+ <div className="flex items-center gap-2.5">
110
+ <Settings className="h-5 w-5 text-gray-700" />
111
+ <h1 className="text-[28px] leading-none font-semibold tracking-[-0.02em] text-gray-900">{t('settings')}</h1>
112
+ </div>
98
113
  </div>
99
- <span className="text-[15px] font-semibold text-gray-800 tracking-[-0.01em]">NextClaw</span>
100
114
  </div>
101
- </div>
115
+ ) : (
116
+ <div className="px-2 mb-8">
117
+ <div className="flex items-center gap-2.5 cursor-pointer">
118
+ <div className="h-7 w-7 rounded-lg overflow-hidden flex items-center justify-center">
119
+ <img src="/logo.svg" alt="NextClaw" className="h-full w-full object-contain" />
120
+ </div>
121
+ <span className="text-[15px] font-semibold text-gray-800 tracking-[-0.01em]">NextClaw</span>
122
+ </div>
123
+ </div>
124
+ )}
102
125
 
103
126
  {/* Navigation */}
104
127
  <nav className="flex-1 flex flex-col">
105
- {/* Core Navigation */}
106
128
  <ul className="space-y-1">
107
- {coreNavItems.map((item) => {
129
+ {navItems.map((item) => {
108
130
  const Icon = item.icon;
109
131
 
110
132
  return (
@@ -129,49 +151,33 @@ export function Sidebar() {
129
151
  )}
130
152
  </NavLink>
131
153
  </li>
132
- );
133
- })}
134
- </ul>
135
-
136
- {/* Advanced Navigation */}
137
- <div className="mt-3 pt-3 border-t border-[#dde0ea]">
138
- <div className="px-3 mb-2">
139
- <span className="text-[11px] font-medium text-gray-400 uppercase tracking-wider">{t('advanced')}</span>
140
- </div>
141
- <ul className="space-y-1">
142
- {advancedNavItems.map((item) => {
143
- const Icon = item.icon;
144
-
145
- return (
146
- <li key={item.target}>
147
- <NavLink
148
- to={item.target}
149
- className={({ isActive }) => cn(
150
- 'group w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-[14px] font-medium transition-all duration-base',
151
- isActive
152
- ? 'bg-gray-200 text-gray-900 font-semibold shadow-sm'
153
- : 'text-gray-600 hover:bg-gray-200/60 hover:text-gray-900'
154
- )}
155
- >
156
- {({ isActive }) => (
157
- <>
158
- <Icon className={cn(
159
- 'h-[17px] w-[17px] transition-colors',
160
- isActive ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-800'
161
- )} />
162
- <span className="flex-1 text-left">{item.label}</span>
163
- </>
164
- )}
165
- </NavLink>
166
- </li>
167
154
  );
168
155
  })}
169
156
  </ul>
170
- </div>
171
157
  </nav>
172
158
 
173
159
  {/* Help Button */}
174
160
  <div className="pt-3 border-t border-[#dde0ea] mt-3">
161
+ {mode === 'main' && (
162
+ <div className="mb-2">
163
+ <NavLink
164
+ to="/settings"
165
+ className={({ isActive }) => cn(
166
+ 'group w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-[14px] font-medium transition-all duration-base',
167
+ isActive
168
+ ? 'bg-gray-200 text-gray-900 font-semibold shadow-sm'
169
+ : 'text-gray-600 hover:bg-[#e4e7ef] hover:text-gray-900'
170
+ )}
171
+ >
172
+ {({ isActive }) => (
173
+ <>
174
+ <Settings className={cn('h-[17px] w-[17px] transition-colors', isActive ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-800')} />
175
+ <span className="flex-1 text-left">{t('settings')}</span>
176
+ </>
177
+ )}
178
+ </NavLink>
179
+ </div>
180
+ )}
175
181
  <div className="mb-2">
176
182
  <Select value={theme} onValueChange={(value) => handleThemeSwitch(value as UiTheme)}>
177
183
  <SelectTrigger className="w-full h-auto rounded-xl border-0 bg-transparent shadow-none px-3 py-2.5 text-[14px] font-medium text-gray-600 hover:bg-[#e4e7ef] focus:ring-0">
@@ -50,6 +50,9 @@ type InstalledRenderEntry = {
50
50
  };
51
51
 
52
52
  type MarketplaceRouteType = 'plugins' | 'skills';
53
+ type MarketplacePageProps = {
54
+ forcedType?: MarketplaceRouteType;
55
+ };
53
56
 
54
57
  function normalizeMarketplaceKey(value: string | undefined): string {
55
58
  return (value ?? '').trim().toLowerCase();
@@ -506,24 +509,31 @@ function PaginationBar(props: {
506
509
  );
507
510
  }
508
511
 
509
- export function MarketplacePage() {
512
+ export function MarketplacePage(props: MarketplacePageProps = {}) {
510
513
  const navigate = useNavigate();
511
514
  const params = useParams<{ type?: string }>();
512
515
  const { language } = useI18n();
513
516
  const docBrowser = useDocBrowser();
517
+ const forcedType = props.forcedType;
514
518
 
515
519
  const routeType: MarketplaceRouteType | null = useMemo(() => {
520
+ if (forcedType === 'plugins' || forcedType === 'skills') {
521
+ return forcedType;
522
+ }
516
523
  if (params.type === 'plugins' || params.type === 'skills') {
517
524
  return params.type;
518
525
  }
519
526
  return null;
520
- }, [params.type]);
527
+ }, [forcedType, params.type]);
521
528
 
522
529
  useEffect(() => {
530
+ if (forcedType) {
531
+ return;
532
+ }
523
533
  if (!routeType) {
524
534
  navigate('/marketplace/plugins', { replace: true });
525
535
  }
526
- }, [routeType, navigate]);
536
+ }, [forcedType, routeType, navigate]);
527
537
 
528
538
  const typeFilter: MarketplaceItemType = routeType === 'skills' ? 'skill' : 'plugin';
529
539
  const localeFallbacks = useMemo(() => buildLocaleFallbacks(language), [language]);
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+ import * as PopoverPrimitive from '@radix-ui/react-popover';
3
+
4
+ import { cn } from '@/lib/utils';
5
+
6
+ const Popover = PopoverPrimitive.Root;
7
+
8
+ const PopoverTrigger = PopoverPrimitive.Trigger;
9
+
10
+ const PopoverAnchor = PopoverPrimitive.Anchor;
11
+
12
+ const PopoverContent = React.forwardRef<
13
+ React.ElementRef<typeof PopoverPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15
+ >(({ className, sideOffset = 8, align = 'start', ...props }, ref) => (
16
+ <PopoverPrimitive.Portal>
17
+ <PopoverPrimitive.Content
18
+ ref={ref}
19
+ sideOffset={sideOffset}
20
+ align={align}
21
+ className={cn(
22
+ 'z-[var(--z-popover,50)] w-72 overflow-hidden rounded-2xl border border-gray-200/50 bg-white p-4 shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ </PopoverPrimitive.Portal>
28
+ ));
29
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30
+
31
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
@@ -17,6 +17,7 @@ import {
17
17
  updateSession,
18
18
  deleteSession,
19
19
  sendChatTurn,
20
+ fetchChatCapabilities,
20
21
  fetchCronJobs,
21
22
  deleteCronJob,
22
23
  setCronJobEnabled,
@@ -242,6 +243,23 @@ export function useSendChatTurn() {
242
243
  });
243
244
  }
244
245
 
246
+ export function useChatCapabilities(params?: { sessionKey?: string | null; agentId?: string | null }) {
247
+ const sessionKey = params?.sessionKey?.trim() || undefined;
248
+ const agentId = params?.agentId?.trim() || undefined;
249
+ return useQuery({
250
+ queryKey: ['chat-capabilities', sessionKey ?? null, agentId ?? null],
251
+ queryFn: async () => {
252
+ try {
253
+ return await fetchChatCapabilities({ sessionKey, agentId });
254
+ } catch {
255
+ return { stopSupported: false };
256
+ }
257
+ },
258
+ staleTime: 10_000,
259
+ retry: false
260
+ });
261
+ }
262
+
245
263
  export function useCronJobs(params: { all?: boolean } = { all: true }) {
246
264
  return useQuery({
247
265
  queryKey: ['cron', params],
package/src/lib/i18n.ts CHANGED
@@ -132,6 +132,8 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
132
132
  runtime: { zh: '路由与运行时', en: 'Routing & Runtime' },
133
133
  marketplace: { zh: '市场', en: 'Marketplace' },
134
134
  advanced: { zh: '高级', en: 'Advanced' },
135
+ settings: { zh: '设置', en: 'Settings' },
136
+ backToMain: { zh: '返回主界面', en: 'Back to Main' },
135
137
 
136
138
  // Common
137
139
  enabled: { zh: '启用', en: 'Enabled' },
@@ -360,6 +362,8 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
360
362
  dmScopeHelp: { zh: '控制私聊会话如何隔离。', en: 'Control how direct-message sessions are isolated.' },
361
363
  defaultContextTokens: { zh: '默认上下文 Token', en: 'Default Context Tokens' },
362
364
  defaultContextTokensHelp: { zh: '当 Agent 未设置单独值时使用该上下文预算。', en: 'Input context budget for agents when no per-agent override is set.' },
365
+ defaultEngine: { zh: '默认引擎', en: 'Default Engine' },
366
+ defaultEngineHelp: { zh: '默认使用的 Agent 引擎类型,例如 native 或 codex-sdk。', en: 'Default agent engine kind, for example native or codex-sdk.' },
363
367
  maxPingPongTurns: { zh: '最大乒乓轮次', en: 'Max Ping-Pong Turns' },
364
368
  maxPingPongTurnsHelp: { zh: '设为 0 可阻止 Agent 间自动 ping-pong。', en: 'Set to 0 to block automatic agent-to-agent ping-pong loops.' },
365
369
  agentList: { zh: 'Agent 列表', en: 'Agent List' },
@@ -375,6 +379,8 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
375
379
  agentIdPlaceholder: { zh: 'Agent ID(例如 engineer)', en: 'Agent ID (e.g. engineer)' },
376
380
  workspaceOverridePlaceholder: { zh: '工作空间覆盖(可选)', en: 'Workspace override (optional)' },
377
381
  modelOverridePlaceholder: { zh: '模型覆盖(可选)', en: 'Model override (optional)' },
382
+ defaultEnginePlaceholder: { zh: '默认引擎(如 native 或 codex-sdk)', en: 'Default engine (e.g. native or codex-sdk)' },
383
+ engineOverridePlaceholder: { zh: '引擎覆盖(可选)', en: 'Engine override (optional)' },
378
384
  contextTokensPlaceholder: { zh: '上下文 tokens', en: 'Context tokens' },
379
385
  maxToolsPlaceholder: { zh: '最大工具次数', en: 'Max tools' },
380
386
  defaultAgent: { zh: '默认 Agent', en: 'Default agent' },
@@ -479,6 +485,15 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
479
485
  chatSearchSessionPlaceholder: { zh: '搜索会话 key / 标签', en: 'Search session key / label' },
480
486
  chatAgentLabel: { zh: '目标 Agent', en: 'Target Agent' },
481
487
  chatSelectAgent: { zh: '选择 Agent', en: 'Select Agent' },
488
+ chatModelLabel: { zh: '对话模型', en: 'Chat Model' },
489
+ chatSelectModel: { zh: '选择模型', en: 'Select model' },
490
+ chatModelNoOptions: { zh: '暂无可用模型,请先配置提供商。', en: 'No available models. Configure a provider first.' },
491
+ chatGoConfigureProvider: { zh: '去配置提供商', en: 'Go to Providers' },
492
+ chatProviderSetupTitle: { zh: '开始前先配置提供商', en: 'Configure a Provider First' },
493
+ chatProviderSetupDescription: {
494
+ zh: '你还没有可用模型。先在提供商页面配置并保存至少一个 Provider 后,再回来开始对话。',
495
+ en: 'No models are available yet. Configure and save at least one provider, then return to start chatting.'
496
+ },
482
497
  chatSessionLabel: { zh: '当前会话', en: 'Current Session' },
483
498
  chatNoSession: { zh: '未选择会话', en: 'No session selected' },
484
499
  chatNoSessionHint: { zh: '创建一个会话并发送第一条消息。', en: 'Create a session and send your first message.' },
@@ -488,6 +503,9 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
488
503
  chatInputPlaceholder: { zh: '输入消息,Enter 发送,Shift + Enter 换行', en: 'Type a message, Enter to send, Shift + Enter for newline' },
489
504
  chatInputHint: { zh: '支持多轮上下文,默认走当前会话。', en: 'Multi-turn context is preserved in the current session.' },
490
505
  chatSend: { zh: '发送', en: 'Send' },
506
+ chatStop: { zh: '停止', en: 'Stop' },
507
+ chatStopPreparing: { zh: '正在建立可停止会话,请稍候…', en: 'Preparing stoppable run…' },
508
+ chatStopUnavailable: { zh: '当前后端引擎不支持手动停止。', en: 'Manual stop is not supported by the current backend engine.' },
491
509
  chatSending: { zh: '发送中...', en: 'Sending...' },
492
510
  chatQueueSend: { zh: '排队发送', en: 'Queue' },
493
511
  chatQueuedHintPrefix: { zh: '当前有', en: 'Queued' },
@@ -502,10 +520,45 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
502
520
  chatRoleMessage: { zh: '消息', en: 'Message' },
503
521
  chatToolCall: { zh: '工具调用', en: 'Tool Call' },
504
522
  chatToolResult: { zh: '工具结果', en: 'Tool Result' },
523
+ chatToolWorkflow: { zh: '工具工作流', en: 'Tool Workflow' },
524
+ chatToolWorkflowDetails: { zh: '展开查看参数和结果', en: 'Expand to view params and results' },
505
525
  chatToolOutput: { zh: '查看输出', en: 'View Output' },
506
526
  chatToolNoOutput: { zh: '无输出(执行完成)', en: 'No output (completed)' },
507
527
  chatReasoning: { zh: '查看推理内容', en: 'Show reasoning' },
508
528
 
529
+ // Chat Sidebar (unified)
530
+ chatSidebarNewTask: { zh: '新任务', en: 'New Task' },
531
+ chatSidebarSearchPlaceholder: { zh: '搜索对话...', en: 'Search conversations...' },
532
+ chatSidebarScheduledTasks: { zh: '定时任务', en: 'Scheduled Tasks' },
533
+ chatSidebarSkills: { zh: '技能', en: 'Skills' },
534
+ chatSidebarTaskRecords: { zh: '会话记录', en: 'Sessions' },
535
+ chatSidebarToday: { zh: '今天', en: 'Today' },
536
+ chatSidebarYesterday: { zh: '昨天', en: 'Yesterday' },
537
+ chatSidebarPrevious7Days: { zh: '近 7 天', en: 'Previous 7 Days' },
538
+ chatSidebarOlder: { zh: '更早', en: 'Older' },
539
+
540
+ // Welcome page
541
+ chatWelcomeTitle: { zh: '你好,有什么可以帮你的吗?', en: 'Hello, how can I help you?' },
542
+ chatWelcomeSubtitle: { zh: '开始一个新任务或选择已有对话', en: 'Start a new task or select an existing conversation' },
543
+ chatWelcomeCapability1Title: { zh: '智能对话', en: 'Smart Conversations' },
544
+ chatWelcomeCapability1Desc: { zh: '多轮上下文对话,支持多种 AI 模型', en: 'Multi-turn context conversations with multiple AI models' },
545
+ chatWelcomeCapability2Title: { zh: '技能扩展', en: 'Skill Extensions' },
546
+ chatWelcomeCapability2Desc: { zh: '通过安装技能扩展 Agent 能力', en: 'Extend Agent capabilities by installing skills' },
547
+ chatWelcomeCapability3Title: { zh: '定时任务', en: 'Scheduled Tasks' },
548
+ chatWelcomeCapability3Desc: { zh: '设置定时执行的自动化任务', en: 'Set up scheduled automated tasks' },
549
+
550
+ // Skills picker
551
+ chatSkillsPickerTitle: { zh: '技能', en: 'Skills' },
552
+ chatSkillsPickerEmpty: { zh: '暂无已安装技能', en: 'No skills installed' },
553
+ chatSkillsPickerSearchPlaceholder: { zh: '搜索技能', en: 'Search skills' },
554
+ chatSkillsPickerNoDescription: { zh: '暂无描述', en: 'No description' },
555
+ chatSkillsPickerOfficial: { zh: '官方', en: 'Official' },
556
+ chatSkillsPickerManage: { zh: '管理技能', en: 'Manage Skills' },
557
+
558
+ // Input bar
559
+ chatInputAttach: { zh: '添加附件', en: 'Attach file' },
560
+ chatInputAttachComingSoon: { zh: '即将支持', en: 'Coming soon' },
561
+
509
562
  // Cron
510
563
  cronPageTitle: { zh: '定时任务', en: 'Cron Jobs' },
511
564
  cronPageDescription: { zh: '查看与删除定时任务,关注执行时间与状态。', en: 'View and delete cron jobs, track schedule and status.' },