@lobehub/lobehub 2.0.0-next.244 → 2.0.0-next.246

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 (28) hide show
  1. package/.github/workflows/bundle-analyzer.yml +2 -2
  2. package/CHANGELOG.md +58 -0
  3. package/apps/desktop/build/Icon-beta.Assets.car +0 -0
  4. package/changelog/v1.json +18 -0
  5. package/docs/development/database-schema.dbml +34 -1
  6. package/package.json +2 -2
  7. package/packages/database/migrations/0067_add_agent_cron_tables.sql +54 -0
  8. package/packages/database/migrations/meta/0067_snapshot.json +10238 -0
  9. package/packages/database/migrations/meta/_journal.json +8 -1
  10. package/packages/database/src/models/__tests__/topics/topic.create.test.ts +4 -0
  11. package/packages/database/src/models/agentCronJob.ts +286 -0
  12. package/packages/database/src/schemas/agentCronJob.ts +138 -0
  13. package/packages/database/src/schemas/index.ts +1 -0
  14. package/packages/database/src/schemas/topic.ts +2 -0
  15. package/packages/database/src/utils/idGenerator.ts +1 -0
  16. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Header/Agent/SwitchPanel.tsx +1 -0
  17. package/src/app/[variants]/(main)/chat/features/Conversation/Header/HeaderActions/index.tsx +1 -1
  18. package/src/features/ChatInput/ActionBar/Knowledge/useControls.tsx +1 -1
  19. package/src/features/ChatInput/ActionBar/Tools/PopoverContent.tsx +92 -0
  20. package/src/features/ChatInput/ActionBar/Tools/ToolItem.tsx +2 -1
  21. package/src/features/ChatInput/ActionBar/Tools/ToolsList.tsx +107 -0
  22. package/src/features/ChatInput/ActionBar/Tools/index.tsx +17 -74
  23. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +2 -20
  24. package/src/features/ChatInput/ActionBar/Upload/ServerMode.tsx +1 -1
  25. package/src/features/ChatInput/ActionBar/components/ActionPopover.tsx +1 -0
  26. package/src/features/ChatInput/ActionBar/components/CheckboxWithLoading.tsx +60 -0
  27. package/src/features/ModelSwitchPanel/index.tsx +1 -0
  28. package/src/features/ChatInput/ActionBar/components/CheckbokWithLoading.tsx +0 -53
@@ -469,7 +469,14 @@
469
469
  "when": 1766474494249,
470
470
  "tag": "0066_add_document_fields",
471
471
  "breakpoints": true
472
+ },
473
+ {
474
+ "idx": 67,
475
+ "version": "7",
476
+ "when": 1767929492232,
477
+ "tag": "0067_add_agent_cron_tables",
478
+ "breakpoints": true
472
479
  }
473
480
  ],
474
481
  "version": "6"
475
- }
482
+ }
@@ -57,6 +57,8 @@ describe('TopicModel - Create', () => {
57
57
  agentId: null,
58
58
  content: null,
59
59
  editorData: null,
60
+ trigger: null,
61
+ mode: null,
60
62
  createdAt: expect.any(Date),
61
63
  updatedAt: expect.any(Date),
62
64
  accessedAt: expect.any(Date),
@@ -96,6 +98,8 @@ describe('TopicModel - Create', () => {
96
98
  groupId: null,
97
99
  historySummary: null,
98
100
  metadata: null,
101
+ trigger: null,
102
+ mode: null,
99
103
  sessionId,
100
104
  userId,
101
105
  createdAt: expect.any(Date),
@@ -0,0 +1,286 @@
1
+ import { and, desc, eq, gt, isNull, or, sql } from 'drizzle-orm';
2
+
3
+ import {
4
+ type AgentCronJob,
5
+ type CreateAgentCronJobData,
6
+ type NewAgentCronJob,
7
+ type UpdateAgentCronJobData,
8
+ agentCronJobs,
9
+ } from '../schemas/agentCronJob';
10
+ import type { LobeChatDatabase } from '../type';
11
+
12
+ export class AgentCronJobModel {
13
+ private readonly userId: string;
14
+ private readonly db: LobeChatDatabase;
15
+
16
+ constructor(db: LobeChatDatabase, userId?: string) {
17
+ this.db = db;
18
+ this.userId = userId!;
19
+ }
20
+
21
+ // Create a new cron job
22
+ async create(data: CreateAgentCronJobData): Promise<AgentCronJob> {
23
+ const cronJob = await this.db
24
+ .insert(agentCronJobs)
25
+ .values({
26
+ ...data,
27
+ // Initialize remaining executions to match max executions
28
+ remainingExecutions: data.maxExecutions,
29
+
30
+ userId: this.userId,
31
+ } as NewAgentCronJob)
32
+ .returning();
33
+
34
+ return cronJob[0];
35
+ }
36
+
37
+ // Find cron job by ID (with user ownership check)
38
+ async findById(id: string): Promise<AgentCronJob | null> {
39
+ const result = await this.db
40
+ .select()
41
+ .from(agentCronJobs)
42
+ .where(and(eq(agentCronJobs.id, id), eq(agentCronJobs.userId, this.userId)))
43
+ .limit(1);
44
+
45
+ return result[0] || null;
46
+ }
47
+
48
+ // Find all cron jobs for a specific agent
49
+ async findByAgentId(agentId: string): Promise<AgentCronJob[]> {
50
+ return this.db
51
+ .select()
52
+ .from(agentCronJobs)
53
+ .where(and(eq(agentCronJobs.agentId, agentId), eq(agentCronJobs.userId, this.userId)))
54
+ .orderBy(desc(agentCronJobs.createdAt));
55
+ }
56
+
57
+ // Find all cron jobs for the user (across all agents)
58
+ async findByUserId(): Promise<AgentCronJob[]> {
59
+ return this.db
60
+ .select()
61
+ .from(agentCronJobs)
62
+ .where(eq(agentCronJobs.userId, this.userId))
63
+ .orderBy(desc(agentCronJobs.lastExecutedAt));
64
+ }
65
+
66
+ // Get all enabled cron jobs (system-wide for execution)
67
+ static async getEnabledJobs(db: LobeChatDatabase): Promise<AgentCronJob[]> {
68
+ return db
69
+ .select()
70
+ .from(agentCronJobs)
71
+ .where(
72
+ and(
73
+ eq(agentCronJobs.enabled, true),
74
+ or(gt(agentCronJobs.remainingExecutions, 0), isNull(agentCronJobs.remainingExecutions)),
75
+ ),
76
+ )
77
+ .orderBy(agentCronJobs.lastExecutedAt);
78
+ }
79
+
80
+ // Update cron job
81
+ async update(id: string, data: UpdateAgentCronJobData): Promise<AgentCronJob | null> {
82
+ const result = await this.db
83
+ .update(agentCronJobs)
84
+ .set({
85
+ ...data,
86
+ updatedAt: new Date(),
87
+ })
88
+ .where(and(eq(agentCronJobs.id, id), eq(agentCronJobs.userId, this.userId)))
89
+ .returning();
90
+
91
+ return result[0] || null;
92
+ }
93
+
94
+ // Delete cron job
95
+ async delete(id: string): Promise<boolean> {
96
+ const result = await this.db
97
+ .delete(agentCronJobs)
98
+ .where(and(eq(agentCronJobs.id, id), eq(agentCronJobs.userId, this.userId)))
99
+ .returning();
100
+
101
+ return result.length > 0;
102
+ }
103
+
104
+ // Update execution statistics after job execution
105
+ static async updateExecutionStats(
106
+ db: LobeChatDatabase,
107
+ jobId: string,
108
+ ): Promise<AgentCronJob | null> {
109
+ // Update execution statistics and decrement remaining executions
110
+ const result = await db
111
+ .update(agentCronJobs)
112
+ .set({
113
+ lastExecutedAt: new Date(),
114
+ remainingExecutions: sql`
115
+ CASE
116
+ WHEN ${agentCronJobs.remainingExecutions} IS NULL THEN NULL
117
+ ELSE ${agentCronJobs.remainingExecutions} - 1
118
+ END
119
+ `,
120
+ totalExecutions: sql`${agentCronJobs.totalExecutions} + 1`,
121
+ updatedAt: new Date(),
122
+ })
123
+ .where(eq(agentCronJobs.id, jobId))
124
+ .returning();
125
+
126
+ const updatedJob = result[0];
127
+
128
+ // Auto-disable job if remaining executions reached 0
129
+ if (updatedJob && updatedJob.remainingExecutions === 0) {
130
+ await db
131
+ .update(agentCronJobs)
132
+ .set({
133
+ enabled: false,
134
+ updatedAt: new Date(),
135
+ })
136
+ .where(eq(agentCronJobs.id, jobId));
137
+
138
+ // Return updated job with enabled = false
139
+ return { ...updatedJob, enabled: false };
140
+ }
141
+
142
+ return updatedJob || null;
143
+ }
144
+
145
+ // Reset execution counts and re-enable job
146
+ async resetExecutions(id: string, newMaxExecutions?: number): Promise<AgentCronJob | null> {
147
+ const result = await this.db
148
+ .update(agentCronJobs)
149
+ .set({
150
+ enabled: true,
151
+ // Re-enable job when resetting
152
+ lastExecutedAt: null,
153
+
154
+ maxExecutions: newMaxExecutions,
155
+
156
+ remainingExecutions: newMaxExecutions,
157
+ totalExecutions: 0,
158
+ updatedAt: new Date(),
159
+ })
160
+ .where(and(eq(agentCronJobs.id, id), eq(agentCronJobs.userId, this.userId)))
161
+ .returning();
162
+
163
+ return result[0] || null;
164
+ }
165
+
166
+ // Get jobs that are near depletion (for warnings)
167
+ async getTasksNearDepletion(threshold: number = 5): Promise<AgentCronJob[]> {
168
+ return this.db
169
+ .select()
170
+ .from(agentCronJobs)
171
+ .where(
172
+ and(
173
+ eq(agentCronJobs.userId, this.userId),
174
+ eq(agentCronJobs.enabled, true),
175
+ gt(agentCronJobs.remainingExecutions, 0),
176
+ sql`${agentCronJobs.remainingExecutions} <= ${threshold}`,
177
+ ),
178
+ )
179
+ .orderBy(agentCronJobs.remainingExecutions);
180
+ }
181
+
182
+ // Get jobs by execution status
183
+ async findByStatus(enabled: boolean): Promise<AgentCronJob[]> {
184
+ return this.db
185
+ .select()
186
+ .from(agentCronJobs)
187
+ .where(and(eq(agentCronJobs.userId, this.userId), eq(agentCronJobs.enabled, enabled)))
188
+ .orderBy(desc(agentCronJobs.updatedAt));
189
+ }
190
+
191
+ // Get execution statistics for dashboard
192
+ async getExecutionStats(): Promise<{
193
+ activeJobs: number;
194
+ completedExecutions: number;
195
+ pendingExecutions: number;
196
+ totalJobs: number;
197
+ }> {
198
+ const result = await this.db
199
+ .select({
200
+ activeJobs: sql<number>`sum(case when ${agentCronJobs.enabled} then 1 else 0 end)`,
201
+ completedExecutions: sql<number>`sum(${agentCronJobs.totalExecutions})`,
202
+ pendingExecutions: sql<number>`
203
+ sum(
204
+ case when ${agentCronJobs.remainingExecutions} is null then 999999
205
+ else coalesce(${agentCronJobs.remainingExecutions}, 0) end
206
+ )
207
+ `,
208
+ totalJobs: sql<number>`count(*)`,
209
+ })
210
+ .from(agentCronJobs)
211
+ .where(eq(agentCronJobs.userId, this.userId));
212
+
213
+ const stats = result[0];
214
+ return {
215
+ activeJobs: Number(stats.activeJobs),
216
+ completedExecutions: Number(stats.completedExecutions),
217
+ pendingExecutions: Number(stats.pendingExecutions === 999_999 ? 0 : stats.pendingExecutions),
218
+ totalJobs: Number(stats.totalJobs),
219
+ };
220
+ }
221
+
222
+ // Batch enable/disable jobs
223
+ async batchUpdateStatus(ids: string[], enabled: boolean): Promise<number> {
224
+ const result = await this.db
225
+ .update(agentCronJobs)
226
+ .set({
227
+ enabled,
228
+ updatedAt: new Date(),
229
+ })
230
+ .where(and(sql`${agentCronJobs.id} = ANY(${ids})`, eq(agentCronJobs.userId, this.userId)))
231
+ .returning();
232
+
233
+ return result.length;
234
+ }
235
+
236
+ // Count total jobs for pagination
237
+ async countByAgentId(agentId: string): Promise<number> {
238
+ const result = await this.db
239
+ .select({ count: sql<number>`count(*)` })
240
+ .from(agentCronJobs)
241
+ .where(and(eq(agentCronJobs.agentId, agentId), eq(agentCronJobs.userId, this.userId)));
242
+
243
+ return Number(result[0].count);
244
+ }
245
+
246
+ // Find jobs with pagination
247
+ async findWithPagination(options: {
248
+ agentId?: string;
249
+ enabled?: boolean;
250
+ limit?: number;
251
+ offset?: number;
252
+ }): Promise<{ jobs: AgentCronJob[]; total: number }> {
253
+ const { agentId, enabled, limit = 20, offset = 0 } = options;
254
+
255
+ const whereConditions = [eq(agentCronJobs.userId, this.userId)];
256
+
257
+ if (agentId) {
258
+ whereConditions.push(eq(agentCronJobs.agentId, agentId));
259
+ }
260
+
261
+ if (enabled !== undefined) {
262
+ whereConditions.push(eq(agentCronJobs.enabled, enabled));
263
+ }
264
+
265
+ const whereClause = and(...whereConditions);
266
+
267
+ // Get total count
268
+ const countResult = await this.db
269
+ .select({ count: sql<number>`count(*)` })
270
+ .from(agentCronJobs)
271
+ .where(whereClause);
272
+
273
+ const total = Number(countResult[0].count);
274
+
275
+ // Get paginated results
276
+ const jobs = await this.db
277
+ .select()
278
+ .from(agentCronJobs)
279
+ .where(whereClause)
280
+ .orderBy(desc(agentCronJobs.createdAt))
281
+ .limit(limit)
282
+ .offset(offset);
283
+
284
+ return { jobs, total };
285
+ }
286
+ }
@@ -0,0 +1,138 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
2
+ import { boolean, index, integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
3
+ import { createInsertSchema } from 'drizzle-zod';
4
+ import { z } from 'zod';
5
+
6
+ import { idGenerator } from '../utils/idGenerator';
7
+ import { timestamps } from './_helpers';
8
+ import { agents } from './agent';
9
+ import { chatGroups } from './chatGroup';
10
+ import { users } from './user';
11
+
12
+ // Execution conditions type for JSONB field
13
+ export interface ExecutionConditions {
14
+ maxExecutionsPerDay?: number;
15
+ timeRange?: {
16
+ end: string; // "18:00"
17
+ start: string; // "09:00"
18
+ };
19
+ weekdays?: number[]; // [1,2,3,4,5] (Monday=1, Sunday=0)
20
+ }
21
+
22
+ // Agent cron jobs table - supports multiple cron jobs per agent
23
+ export const agentCronJobs = pgTable(
24
+ 'agent_cron_jobs',
25
+ {
26
+ id: text('id')
27
+ .primaryKey()
28
+ .$defaultFn(() => idGenerator('agentCronJobs'))
29
+ .notNull(),
30
+
31
+ // Foreign keys
32
+ agentId: text('agent_id')
33
+ .references(() => agents.id, { onDelete: 'cascade' })
34
+ .notNull(),
35
+ groupId: text('group_id')
36
+ .references(() => chatGroups.id, { onDelete: 'cascade' }),
37
+ userId: text('user_id')
38
+ .references(() => users.id, { onDelete: 'cascade' })
39
+ .notNull(),
40
+
41
+ // Task identification
42
+ name: text('name'), // Optional task name like "Daily Report", "Data Monitoring"
43
+ description: text('description'), // Optional task description
44
+
45
+ // Core configuration
46
+ enabled: boolean('enabled').default(true),
47
+ cronPattern: text('cron_pattern').notNull(), // e.g., "0 */30 * * *"
48
+ timezone: text('timezone').default('UTC'),
49
+
50
+ // Content fields
51
+ content: text('content').notNull(), // Simple text content
52
+ editData: jsonb('edit_data'), // Rich content data (markdown, files, images, etc.)
53
+
54
+ // Execution count management
55
+ maxExecutions: integer('max_executions'), // null = unlimited
56
+ remainingExecutions: integer('remaining_executions'), // null = unlimited
57
+
58
+ // Execution conditions (stored as JSONB)
59
+ executionConditions: jsonb('execution_conditions').$type<ExecutionConditions>(),
60
+
61
+ // Execution statistics
62
+ lastExecutedAt: timestamp('last_executed_at'),
63
+ totalExecutions: integer('total_executions').default(0),
64
+
65
+ ...timestamps,
66
+ },
67
+ (t) => [
68
+ // Indexes for performance
69
+ index('agent_cron_jobs_agent_id_idx').on(t.agentId),
70
+ index('agent_cron_jobs_group_id_idx').on(t.groupId),
71
+ index('agent_cron_jobs_user_id_idx').on(t.userId),
72
+ index('agent_cron_jobs_enabled_idx').on(t.enabled),
73
+ index('agent_cron_jobs_remaining_executions_idx').on(t.remainingExecutions),
74
+ index('agent_cron_jobs_last_executed_at_idx').on(t.lastExecutedAt),
75
+ ],
76
+ );
77
+
78
+ // Validation schemas
79
+ export const cronPatternSchema = z
80
+ .string()
81
+ .regex(
82
+ /^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})$/,
83
+ 'Invalid cron pattern',
84
+ );
85
+
86
+ // Minimum 30 minutes validation
87
+ export const minimumIntervalSchema = z.string().refine((pattern) => {
88
+ // For simplicity, we'll validate common patterns
89
+ // More complex validation can be added later
90
+ const thirtyMinPatterns = [
91
+ '0 */30 * * *', // Every 30 minutes
92
+ '0 0 * * *', // Every hour
93
+ '0 0 */2 * *', // Every 2 hours
94
+ '0 0 */6 * *', // Every 6 hours
95
+ '0 0 0 * *', // Daily
96
+ '0 0 0 * * 1', // Weekly
97
+ '0 0 0 1 *', // Monthly
98
+ ];
99
+
100
+ // Check if it matches allowed patterns or follows 30+ minute intervals
101
+ return (
102
+ thirtyMinPatterns.includes(pattern) ||
103
+ pattern.includes('*/30') ||
104
+ pattern.includes('*/60') ||
105
+ /0 \d+ \* \* \*/.test(pattern)
106
+ ); // Hours pattern
107
+ }, 'Minimum execution interval is 30 minutes');
108
+
109
+ export const executionConditionsSchema = z
110
+ .object({
111
+ maxExecutionsPerDay: z.number().min(1).max(100).optional(),
112
+ timeRange: z
113
+ .object({
114
+ end: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
115
+ start: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
116
+ })
117
+ .optional(),
118
+ weekdays: z.array(z.number().min(0).max(6)).optional(),
119
+ })
120
+ .optional();
121
+
122
+ export const insertAgentCronJobSchema = createInsertSchema(agentCronJobs, {
123
+ cronPattern: minimumIntervalSchema,
124
+ content: z.string().min(1).max(2000),
125
+ editData: z.record(z.any()).optional(), // Allow any JSON structure for rich content
126
+ name: z.string().max(100).optional(),
127
+ description: z.string().max(500).optional(),
128
+ maxExecutions: z.number().min(1).max(10_000).optional(),
129
+ executionConditions: executionConditionsSchema,
130
+ });
131
+
132
+ export const updateAgentCronJobSchema = insertAgentCronJobSchema.partial();
133
+
134
+ // Type exports
135
+ export type NewAgentCronJob = typeof agentCronJobs.$inferInsert;
136
+ export type AgentCronJob = typeof agentCronJobs.$inferSelect;
137
+ export type CreateAgentCronJobData = z.infer<typeof insertAgentCronJobSchema>;
138
+ export type UpdateAgentCronJobData = z.infer<typeof updateAgentCronJobSchema>;
@@ -1,4 +1,5 @@
1
1
  export * from './agent';
2
+ export * from './agentCronJob';
2
3
  export * from './aiInfra';
3
4
  export * from './apiKey';
4
5
  export * from './asyncTask';
@@ -31,6 +31,8 @@ export const topics = pgTable(
31
31
  clientId: text('client_id'),
32
32
  historySummary: text('history_summary'),
33
33
  metadata: jsonb('metadata').$type<ChatTopicMetadata | undefined>(),
34
+ trigger: text('trigger'), // 'cron' | 'chat' | 'api' - topic creation trigger source
35
+ mode: text('mode'), // 'temp' | 'test' | 'default' - topic usage scenario
34
36
  ...timestamps,
35
37
  },
36
38
  (t) => [
@@ -6,6 +6,7 @@ export const createNanoId = (size = 8) =>
6
6
  customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', size);
7
7
 
8
8
  const prefixes = {
9
+ agentCronJobs: 'cron',
9
10
  agents: 'agt',
10
11
  budget: 'bgt',
11
12
  chatGroups: 'cg',
@@ -42,6 +42,7 @@ const SwitchPanel = memo<PropsWithChildren>(({ children }) => {
42
42
  <Popover
43
43
  classNames={{ trigger: styles.trigger }}
44
44
  content={content}
45
+ nativeButton={false}
45
46
  placement="bottomLeft"
46
47
  styles={{
47
48
  content: {
@@ -12,7 +12,7 @@ const HeaderActions = memo(() => {
12
12
  const { menuItems } = useMenu();
13
13
 
14
14
  return (
15
- <DropdownMenu items={menuItems}>
15
+ <DropdownMenu items={menuItems} nativeButton={false}>
16
16
  <ActionIcon icon={MoreHorizontal} size={DESKTOP_HEADER_ICON_SIZE} />
17
17
  </DropdownMenu>
18
18
  );
@@ -9,7 +9,7 @@ import { useAgentStore } from '@/store/agent';
9
9
  import { agentByIdSelectors } from '@/store/agent/selectors';
10
10
 
11
11
  import { useAgentId } from '../../hooks/useAgentId';
12
- import CheckboxItem from '../components/CheckbokWithLoading';
12
+ import CheckboxItem from '../components/CheckboxWithLoading';
13
13
 
14
14
  export const useControls = ({
15
15
  setModalOpen,
@@ -0,0 +1,92 @@
1
+ import { Flexbox, Icon, type ItemType, Segmented, usePopoverContext } from '@lobehub/ui';
2
+ import { createStaticStyles, cssVar } from 'antd-style';
3
+ import { ChevronRight, Store } from 'lucide-react';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import ToolsList, { toolsListStyles } from './ToolsList';
8
+
9
+ const styles = createStaticStyles(({ css }) => ({
10
+ footer: css`
11
+ padding: 4px;
12
+ border-block-start: 1px solid ${cssVar.colorBorderSecondary};
13
+ `,
14
+ header: css`
15
+ padding: 8px;
16
+ border-block-end: 1px solid ${cssVar.colorBorderSecondary};
17
+ `,
18
+ trailingIcon: css`
19
+ opacity: 0.5;
20
+ `,
21
+ }));
22
+
23
+ type TabType = 'all' | 'installed';
24
+
25
+ interface PopoverContentProps {
26
+ activeTab: TabType;
27
+ currentItems: ItemType[];
28
+ enableKlavis: boolean;
29
+ onOpenStore: () => void;
30
+ onTabChange: (tab: TabType) => void;
31
+ }
32
+
33
+ const PopoverContent = memo<PopoverContentProps>(
34
+ ({ activeTab, currentItems, enableKlavis, onTabChange, onOpenStore }) => {
35
+ const { t } = useTranslation('setting');
36
+
37
+ const { close: closePopover } = usePopoverContext();
38
+
39
+ return (
40
+ <Flexbox gap={0}>
41
+ <div className={styles.header}>
42
+ <Segmented
43
+ block
44
+ onChange={(v) => onTabChange(v as TabType)}
45
+ options={[
46
+ {
47
+ label: t('tools.tabs.all', { defaultValue: 'all' }),
48
+ value: 'all',
49
+ },
50
+ {
51
+ label: t('tools.tabs.installed', { defaultValue: 'Installed' }),
52
+ value: 'installed',
53
+ },
54
+ ]}
55
+ size="small"
56
+ value={activeTab}
57
+ />
58
+ </div>
59
+ <div
60
+ style={{
61
+ maxHeight: 500,
62
+ minHeight: enableKlavis ? 500 : undefined,
63
+ overflowY: 'auto',
64
+ }}
65
+ >
66
+ <ToolsList items={currentItems} />
67
+ </div>
68
+ <div className={styles.footer}>
69
+ <div
70
+ className={toolsListStyles.item}
71
+ onClick={() => {
72
+ closePopover();
73
+ onOpenStore();
74
+ }}
75
+ role="button"
76
+ tabIndex={0}
77
+ >
78
+ <div className={toolsListStyles.itemIcon}>
79
+ <Icon icon={Store} size={20} />
80
+ </div>
81
+ <div className={toolsListStyles.itemContent}>{t('tools.plugins.store')}</div>
82
+ <Icon className={styles.trailingIcon} icon={ChevronRight} size={16} />
83
+ </div>
84
+ </div>
85
+ </Flexbox>
86
+ );
87
+ },
88
+ );
89
+
90
+ PopoverContent.displayName = 'PopoverContent';
91
+
92
+ export default PopoverContent;
@@ -5,7 +5,7 @@ import PluginTag from '@/components/Plugins/PluginTag';
5
5
  import { useToolStore } from '@/store/tool';
6
6
  import { customPluginSelectors } from '@/store/tool/selectors';
7
7
 
8
- import CheckboxItem, { type CheckboxItemProps } from '../components/CheckbokWithLoading';
8
+ import CheckboxItem, { type CheckboxItemProps } from '../components/CheckboxWithLoading';
9
9
 
10
10
  const ToolItem = memo<CheckboxItemProps>(({ id, onUpdate, label, checked }) => {
11
11
  const isCustom = useToolStore((s) => customPluginSelectors.isCustomPlugin(id)(s));
@@ -13,6 +13,7 @@ const ToolItem = memo<CheckboxItemProps>(({ id, onUpdate, label, checked }) => {
13
13
  return (
14
14
  <CheckboxItem
15
15
  checked={checked}
16
+ hasPadding={false}
16
17
  id={id}
17
18
  label={
18
19
  <Flexbox align={'center'} gap={8} horizontal>