Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new

This commit is contained in:
icarus 2025-09-18 17:12:10 +08:00
commit 6e89d0037f
13 changed files with 154 additions and 244 deletions

View File

@ -328,4 +328,4 @@
"internal": {
"indexes": {}
}
}
}

View File

@ -10,4 +10,4 @@
"breakpoints": true
}
]
}
}

View File

@ -35,6 +35,9 @@ const allX64 = {
'@napi-rs/system-ocr-win32-x64-msvc': '1.0.2'
}
const claudeCodeVenderPath = '@anthropic-ai/claude-code/vendor'
const claudeCodeVenders = ['arm64-darwin', 'arm64-linux', 'x64-darwin', 'x64-linux', 'x64-win32']
const platformToArch = {
mac: 'darwin',
windows: 'win32',
@ -46,9 +49,6 @@ exports.default = async function (context) {
const archType = arch === Arch.arm64 ? 'arm64' : 'x64'
const platform = context.packager.platform.name
const arm64Filters = Object.keys(allArm64).map((f) => '!node_modules/' + f + '/**')
const x64Filters = Object.keys(allX64).map((f) => '!node_modules/' + f + '/*')
const downloadPackages = async (packages) => {
console.log('downloading packages ......')
const downloadPromises = []
@ -67,25 +67,39 @@ exports.default = async function (context) {
await Promise.all(downloadPromises)
}
const changeFilters = async (packages, filtersToExclude, filtersToInclude) => {
await downloadPackages(packages)
const changeFilters = async (filtersToExclude, filtersToInclude) => {
// remove filters for the target architecture (allow inclusion)
let filters = context.packager.config.files[0].filter
filters = filters.filter((filter) => !filtersToInclude.includes(filter))
// add filters for other architectures (exclude them)
filters.push(...filtersToExclude)
context.packager.config.files[0].filter = filters
}
if (arch === Arch.arm64) {
await changeFilters(allArm64, x64Filters, arm64Filters)
return
}
await downloadPackages(arch === Arch.arm64 ? allArm64 : allX64)
if (arch === Arch.x64) {
await changeFilters(allX64, arm64Filters, x64Filters)
return
const arm64Filters = Object.keys(allArm64).map((f) => '!node_modules/' + f + '/**')
const x64Filters = Object.keys(allX64).map((f) => '!node_modules/' + f + '/*')
const excludeClaudeCodeRipgrepFilters = claudeCodeVenders
.filter((f) => f !== `${archType}-${platformToArch[platform]}`)
.map((f) => '!node_modules/' + claudeCodeVenderPath + '/ripgrep/' + f + '/**')
const excludeClaudeCodeJBPlutins = ['!node_modules/' + claudeCodeVenderPath + '/' + 'claude-code-jetbrains-plugin']
const includeClaudeCodeFilters = [
'!node_modules/' + claudeCodeVenderPath + '/' + `${archType}-${platformToArch[platform]}/**`
]
if (arch === Arch.arm64) {
await changeFilters(
[...x64Filters, ...excludeClaudeCodeRipgrepFilters, ...excludeClaudeCodeJBPlutins],
[...arm64Filters, ...includeClaudeCodeFilters]
)
} else {
await changeFilters(
[...arm64Filters, ...excludeClaudeCodeRipgrepFilters, ...excludeClaudeCodeJBPlutins],
[...x64Filters, ...includeClaudeCodeFilters]
)
}
}

View File

@ -36,10 +36,7 @@ export const createMessage = async (req: Request, res: Response): Promise<void>
logger.debug('Streaming message data:', messageData)
// Step 1: Save user message first
const userMessage = await sessionMessageService.saveUserMessage(
sessionId,
messageData.content
)
const userMessage = await sessionMessageService.saveUserMessage(sessionId, messageData.content)
// Set SSE headers
res.setHeader('Content-Type', 'text/event-stream')
@ -48,7 +45,6 @@ export const createMessage = async (req: Request, res: Response): Promise<void>
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', 'Cache-Control')
const messageStream = sessionMessageService.createSessionMessage(session, messageData, userMessage.id)
// Track stream lifecycle so we keep the SSE connection open until persistence finishes

View File

@ -51,9 +51,7 @@ export class MigrationService {
}
// Get applied migrations
const appliedMigrations = hasMigrationsTable
? await this.getAppliedMigrations()
: []
const appliedMigrations = hasMigrationsTable ? await this.getAppliedMigrations() : []
const appliedVersions = new Set(appliedMigrations.map((m) => Number(m.version)))
const latestAppliedVersion = appliedMigrations.reduce(
@ -90,9 +88,7 @@ export class MigrationService {
private async migrationsTableExists(): Promise<boolean> {
try {
const table = await this.client.execute(
`SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'`
)
const table = await this.client.execute(`SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'`)
return table.rows.length > 0
} catch (error) {
logger.error('Failed to check migrations table status:', { error })
@ -162,5 +158,4 @@ export class MigrationService {
throw error
}
}
}

View File

@ -11,4 +11,4 @@ export const migrations = sqliteTable('migrations', {
})
export type Migration = typeof migrations.$inferSelect
export type NewMigration = typeof migrations.$inferInsert
export type NewMigration = typeof migrations.$inferInsert

View File

@ -1,11 +1,12 @@
import { EventEmitter } from 'node:events'
import { PermissionMode } from '@anthropic-ai/claude-code'
import { loggerService } from '@logger'
import type {
AgentSessionMessageEntity,
CreateSessionMessageRequest,
GetAgentSessionResponse,
ListOptions,
ListOptions
} from '@types'
import { ModelMessage, UIMessage, UIMessageChunk } from 'ai'
import { convertToModelMessages, readUIMessageStream } from 'ai'
@ -17,7 +18,6 @@ import ClaudeCodeService from './claudecode'
const logger = loggerService.withContext('SessionMessageService')
// Collapse a UIMessageChunk stream into a final UIMessage, then convert to ModelMessage[]
export async function chunksToModelMessages(
chunkStream: ReadableStream<UIMessageChunk>,
@ -68,7 +68,6 @@ interface PersistContext {
session: GetAgentSessionResponse
accumulator: ChunkAccumulator
userMessageId: number
sessionStream: EventEmitter
}
// Chunk accumulator class to collect and reconstruct streaming data
@ -254,10 +253,7 @@ export class SessionMessageService extends BaseService {
updated_at: now
}
const [saved] = await this.database
.insert(sessionMessagesTable)
.values(insertData)
.returning()
const [saved] = await this.database.insert(sessionMessagesTable).values(insertData).returning()
return this.deserializeSessionMessage(saved) as AgentSessionMessageEntity
}
@ -299,8 +295,8 @@ export class SessionMessageService extends BaseService {
// Create the streaming agent invocation (using invokeStream for streaming)
const claudeStream = this.cc.invoke(req.content, session.accessible_paths[0], session_id, {
permissionMode: session.configuration?.permissionMode || 'default',
maxTurns: session.configuration?.maxTurns || 10
permissionMode: (session.configuration?.permissionMode as PermissionMode) || 'default',
maxTurns: (session.configuration?.maxTurns as number) || 10
})
// Use chunk accumulator to manage streaming data
@ -345,8 +341,7 @@ export class SessionMessageService extends BaseService {
void this.persistSessionMessageAsync({
session,
accumulator,
userMessageId,
sessionStream
userMessageId
})
}
@ -355,6 +350,10 @@ export class SessionMessageService extends BaseService {
error: serializeError(underlyingError),
persistScheduled
})
// Always emit a finish chunk at the end
sessionStream.emit('data', {
type: 'finish'
})
break
}
@ -367,18 +366,15 @@ export class SessionMessageService extends BaseService {
// Set the agent result in the accumulator
accumulator.setAgentResult(event.agentResult)
// // Emit SSE completion FIRST before persistence
// sessionStream.emit('data', {
// type: 'complete',
// result: accumulator.buildStructuredContent()
// })
// Then handle async persistence
void this.persistSessionMessageAsync({
session,
accumulator,
userMessageId,
sessionStream
userMessageId
})
// Always emit a finish chunk at the end
sessionStream.emit('data', {
type: 'finish'
})
break
}
@ -399,11 +395,10 @@ export class SessionMessageService extends BaseService {
})
}
private async persistSessionMessageAsync({ session, accumulator, userMessageId, sessionStream }: PersistContext) {
private async persistSessionMessageAsync({ session, accumulator, userMessageId }: PersistContext) {
if (!session?.id) {
const missingSessionError = new Error('Missing session_id for persisted message')
logger.error(missingSessionError.message, { error: missingSessionError })
sessionStream.emit('data', { type: 'persist-error', error: serializeError(missingSessionError) })
logger.error('error persisting session message', { error: missingSessionError })
return
}
@ -435,13 +430,10 @@ export class SessionMessageService extends BaseService {
updated_at: now
}
const [row] = await this.database.insert(sessionMessagesTable).values(insertData).returning()
const entity = this.deserializeSessionMessage(row) as AgentSessionMessageEntity
sessionStream.emit('data', { type: 'persisted', message: entity })
await this.database.insert(sessionMessagesTable).values(insertData).returning()
logger.debug('Success Persisted session message')
} catch (error) {
logger.error('Failed to persist session message', { error })
sessionStream.emit('data', { type: 'persist-error', error: serializeError(error) })
}
}

View File

@ -11,7 +11,6 @@ import { and, count, eq, type SQL } from 'drizzle-orm'
import { BaseService } from '../BaseService'
import { agentsTable, type InsertSessionRow, type SessionRow, sessionsTable } from '../database/schema'
export class SessionService extends BaseService {
private static instance: SessionService | null = null

View File

@ -1,9 +1,8 @@
// src/main/services/agents/services/claudecode/index.ts
import { ChildProcess, spawn } from 'node:child_process'
import { EventEmitter } from 'node:events'
import { createRequire } from 'node:module'
import { Options, SDKMessage } from '@anthropic-ai/claude-code'
import { Options, query, SDKMessage } from '@anthropic-ai/claude-code'
import { loggerService } from '@logger'
import { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
@ -38,210 +37,129 @@ class ClaudeCodeService implements AgentServiceInterface {
invoke(prompt: string, cwd: string, session_id?: string, base?: Options): AgentStream {
const aiStream = new ClaudeCodeStream()
// Spawn process with same parameters as invoke
const args: string[] = [this.claudeExecutablePath, '--output-format', 'stream-json', '--verbose']
// Build SDK options from parameters
const options: Options = {
cwd,
pathToClaudeCodeExecutable: this.claudeExecutablePath,
stderr: (chunk: string) => {
logger.info('claude stderr', { chunk })
},
...base
}
if (session_id) {
args.push('--resume', session_id)
}
if (base?.maxTurns) {
args.push('--max-turns', base.maxTurns.toString())
}
if (base?.permissionMode) {
args.push('--permission-mode', base.permissionMode)
options.resume = session_id
}
args.push('--print', prompt)
logger.info('Spawning Claude Code streaming process', { args, cwd })
const p = spawn(process.execPath, args, {
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
cwd,
stdio: ['pipe', 'pipe', 'pipe'],
shell: false,
detached: false
logger.info('Starting Claude Code SDK query', {
prompt,
options: { cwd, maxTurns: options.maxTurns, permissionMode: options.permissionMode }
})
logger.info('Streaming process created', { pid: p.pid })
// Close stdin immediately
if (p.stdin) {
p.stdin.end()
logger.debug('Closed stdin for streaming process')
}
this.setupStreamingHandlers(p, aiStream)
// Start async processing
this.processSDKQuery(prompt, options, aiStream)
return aiStream
}
/**
* Set up process event handlers for streaming output
* Process SDK query and emit stream events
*/
private setupStreamingHandlers(process: ChildProcess, stream: ClaudeCodeStream): void {
let stdoutData = ''
let stderrData = ''
const jsonOutput: any[] = []
private async processSDKQuery(prompt: string, options: Options, stream: ClaudeCodeStream): Promise<void> {
const jsonOutput: SDKMessage[] = []
let hasCompleted = false
let stdoutBuffer = ''
const startTime = Date.now()
const emitChunks = (sdkMessage: SDKMessage) => {
jsonOutput.push(sdkMessage)
const chunks = transformSDKMessageToUIChunk(sdkMessage)
for (const chunk of chunks) {
stream.emit('data', {
type: 'chunk',
chunk,
rawAgentMessage: sdkMessage // Store Claude Code specific SDKMessage as generic agent message
})
try {
// Process streaming responses using SDK query
for await (const message of query({
prompt,
options
})) {
if (hasCompleted) break
jsonOutput.push(message)
logger.silly('claude response', { message })
if (message.type === 'assistant' || message.type === 'user') {
logger.silly('message content', {
message: JSON.stringify({ role: message.message.role, content: message.message.content })
})
}
// Transform SDKMessage to UIMessageChunks
const chunks = transformSDKMessageToUIChunk(message)
for (const chunk of chunks) {
stream.emit('data', {
type: 'chunk',
chunk,
rawAgentMessage: message
})
}
}
}
// Handle stdout with streaming events
if (process.stdout) {
process.stdout.setEncoding('utf8')
process.stdout.on('data', (data: string) => {
stdoutData += data
stdoutBuffer += data
logger.debug('Streaming stdout chunk:', { length: data.length })
let newlineIndex = stdoutBuffer.indexOf('\n')
while (newlineIndex !== -1) {
const line = stdoutBuffer.slice(0, newlineIndex)
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1)
const trimmed = line.trim()
if (trimmed) {
try {
const parsed = JSON.parse(trimmed) as SDKMessage
emitChunks(parsed)
logger.debug('Parsed JSON line', { parsed })
} catch (error) {
logger.debug('Non-JSON line', { line: trimmed })
}
}
newlineIndex = stdoutBuffer.indexOf('\n')
}
})
process.stdout.on('end', () => {
const trimmed = stdoutBuffer.trim()
if (trimmed) {
try {
const parsed = JSON.parse(trimmed) as SDKMessage
emitChunks(parsed)
logger.debug('Parsed JSON line on stream end', { parsed })
} catch (error) {
logger.debug('Non-JSON remainder on stdout end', { line: trimmed })
}
}
logger.debug('Streaming stdout ended')
})
}
// Handle stderr
if (process.stderr) {
process.stderr.setEncoding('utf8')
process.stderr.on('data', (data: string) => {
stderrData += data
const message = data.trim()
if (!message) return
logger.warn('Streaming stderr chunk:', { data: message })
stream.emit('data', {
type: 'error',
error: new Error(message)
})
})
process.stderr.on('end', () => {
logger.debug('Streaming stderr ended')
})
}
// Handle process completion
const completeProcess = (code: number | null, signal: NodeJS.Signals | null, error?: Error) => {
if (hasCompleted) return
// Successfully completed
hasCompleted = true
const duration = Date.now() - startTime
const success = !error && code === 0
logger.info('Streaming process completed', {
code,
signal,
success,
logger.debug('SDK query completed successfully', {
duration,
stdoutLength: stdoutData.length,
stderrLength: stderrData.length,
jsonItems: jsonOutput.length,
error: error?.message
messageCount: jsonOutput.length
})
const result: ClaudeCodeResult = {
success,
stdout: stdoutData,
stderr: stderrData,
success: true,
stdout: '',
stderr: '',
jsonOutput,
exitCode: code || undefined,
error
exitCode: 0
}
// Emit completion event with agent-specific result
// Emit completion event
stream.emit('data', {
type: 'complete',
agentResult: {
...result,
rawSDKMessages: jsonOutput, // Claude Code specific: all collected SDK messages
agentType: 'claude-code' // Identify the agent type
rawSDKMessages: jsonOutput,
agentType: 'claude-code'
}
})
} catch (error) {
if (hasCompleted) return
hasCompleted = true
const duration = Date.now() - startTime
logger.error('SDK query error:', {
error: error instanceof Error ? error.message : String(error),
duration,
messageCount: jsonOutput.length
})
const result: ClaudeCodeResult = {
success: false,
stdout: '',
stderr: error instanceof Error ? error.message : String(error),
jsonOutput,
error: error instanceof Error ? error : new Error(String(error)),
exitCode: 1
}
// Emit error event
stream.emit('data', {
type: 'error',
error: error instanceof Error ? error : new Error(String(error))
})
// Emit completion with error result
stream.emit('data', {
type: 'complete',
agentResult: {
...result,
rawSDKMessages: jsonOutput,
agentType: 'claude-code'
}
})
}
// Handle process exit
process.on('exit', (code, signal) => {
completeProcess(code, signal)
})
// Handle process errors
process.on('error', (error) => {
const duration = Date.now() - startTime
logger.error('Streaming process error:', {
error: error.message,
duration,
stdoutLength: stdoutData.length,
stderrLength: stderrData.length
})
completeProcess(null, null, error)
})
// Handle close event as a fallback
process.on('close', (code, signal) => {
logger.debug('Streaming process closed', { code, signal })
completeProcess(code, signal)
})
// Set timeout to prevent hanging
const timeout = setTimeout(() => {
if (!hasCompleted) {
logger.error('Streaming process timeout after 600 seconds', {
pid: process.pid,
stdoutLength: stdoutData.length,
stderrLength: stderrData.length,
jsonItems: jsonOutput.length
})
process.kill('SIGTERM')
completeProcess(null, null, new Error('Process timeout after 600 seconds'))
}
}, 600 * 1000)
// Clear timeout when process ends
process.on('exit', () => clearTimeout(timeout))
process.on('error', () => clearTimeout(timeout))
}
}
export default ClaudeCodeService

View File

@ -327,11 +327,6 @@ function handleResultMessage(message: Extract<SDKMessage, { type: 'result' }>):
}
}
})
// Always emit a finish chunk at the end
chunks.push({
type: 'finish'
})
return chunks
}

View File

@ -16,10 +16,11 @@ export { sessionMessageService } from './SessionMessageService'
export { sessionService } from './SessionService'
// Type definitions for service requests and responses
export type { AgentEntity, AgentSessionEntity,CreateAgentRequest, UpdateAgentRequest } from '@types'
export type { AgentEntity, AgentSessionEntity, CreateAgentRequest, UpdateAgentRequest } from '@types'
export type {
AgentSessionMessageEntity,
CreateSessionRequest,
GetAgentSessionResponse,
ListOptions as SessionListOptions,
UpdateSessionRequest} from '@types'
UpdateSessionRequest
} from '@types'

View File

@ -2486,7 +2486,7 @@ const migrateConfig = {
return state
}
},
'156': (state: RootState) => {
'157': (state: RootState) => {
try {
state.llm.providers.forEach((provider) => {
if (provider.id === SystemProviderIds.anthropic) {
@ -2497,7 +2497,7 @@ const migrateConfig = {
})
return state
} catch (error) {
logger.error('migrate 156 error', error as Error)
logger.error('migrate 157 error', error as Error)
return state
}
}

View File

@ -42,7 +42,7 @@ function ContextMenuSubTrigger({
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[inset]:pl-8 data-[state=open]:text-accent-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...props}>
@ -57,7 +57,7 @@ function ContextMenuSubContent({ className, ...props }: React.ComponentProps<typ
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
'z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-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 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in',
className
)}
{...props}
@ -71,7 +71,7 @@ function ContextMenuContent({ className, ...props }: React.ComponentProps<typeof
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
'z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-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 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in',
className
)}
{...props}
@ -95,7 +95,7 @@ function ContextMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:!text-destructive",
"data-[variant=destructive]:*:[svg]:!text-destructive relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[disabled]:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...props}
@ -113,7 +113,7 @@ function ContextMenuCheckboxItem({
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
checked={checked}
@ -137,7 +137,7 @@ function ContextMenuRadioItem({
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...props}>
@ -162,7 +162,7 @@ function ContextMenuLabel({
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn('px-2 py-1.5 text-sm font-medium text-foreground data-[inset]:pl-8', className)}
className={cn('px-2 py-1.5 font-medium text-foreground text-sm data-[inset]:pl-8', className)}
{...props}
/>
)
@ -182,7 +182,7 @@ function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span
return (
<span
data-slot="context-menu-shortcut"
className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
className={cn('ml-auto text-muted-foreground text-xs tracking-widest', className)}
{...props}
/>
)