cc-haha/src/server/ws/events.ts
程序员阿江(Relakkes) f41e786748 Align desktop file mentions with CLI search
Desktop @ file search was recursively walking the filesystem with a local skip list, which let Python and Node generated directories leak into results and diverged from CLI behavior. Route candidate discovery through the same git-first model: tracked files, untracked files with exclude-standard, and ripgrep fallback for non-git folders, then derive selectable directories from those candidates.

Constraint: Desktop picker must select both files and directories without surfacing ignored project artifacts.
Constraint: No new dependencies; reuse the existing git, ripgrep, settings, and ignore utilities.
Rejected: Maintain a hardcoded directory denylist | it would drift from CLI and miss project-specific ignore rules.
Rejected: Full recursive readdir scanning | it ignores git index semantics and makes large dependency trees visible.
Confidence: high
Scope-risk: moderate
Directive: Keep desktop @ file candidate discovery aligned with src/hooks/fileSuggestions.ts before changing ranking or ignore behavior.
Tested: bun test src/server/__tests__/filesystem.test.ts
Tested: cd desktop && bun run test -- FileSearchMenu.test.tsx ChatInput.test.tsx
Tested: cd desktop && bun run lint && bun run test -- --run && bun run build
Tested: bun run check:server
Tested: bun run check:coverage
Tested: bun run verify
Not-tested: Native Windows/Linux manual UI smoke; path handling relies on cross-platform git/ripgrep wrappers and normalized relative paths.
2026-05-13 10:38:13 +08:00

151 lines
4.5 KiB
TypeScript

/**
* WebSocket event type definitions
*
* 定义客户端与服务器之间 WebSocket 通信的消息类型。
*/
// ============================================================================
// Client → Server
// ============================================================================
export type ClientMessage =
| { type: 'prewarm_session' }
| { type: 'user_message'; content: string; attachments?: AttachmentRef[] }
| {
type: 'permission_response'
requestId: string
allowed: boolean
rule?: string
updatedInput?: Record<string, unknown>
}
| {
type: 'computer_use_permission_response'
requestId: string
response: ComputerUsePermissionResponse
}
| { type: 'set_permission_mode'; mode: string }
| { type: 'set_runtime_config'; providerId: string | null; modelId: string }
| { type: 'stop_generation' }
| { type: 'ping' }
export type AttachmentRef = {
type: 'file' | 'image'
name?: string
path?: string
data?: string // base64 for images
mimeType?: string
isDirectory?: boolean
}
// ============================================================================
// Server → Client
// ============================================================================
export type ServerMessage =
| { type: 'connected'; sessionId: string }
| { type: 'content_start'; blockType: 'text' | 'tool_use'; toolName?: string; toolUseId?: string; parentToolUseId?: string }
| { type: 'content_delta'; text?: string; toolInput?: string }
| { type: 'tool_use_complete'; toolName: string; toolUseId: string; input: unknown; parentToolUseId?: string }
| { type: 'tool_result'; toolUseId: string; content: unknown; isError: boolean; parentToolUseId?: string }
| {
type: 'permission_request'
requestId: string
toolName: string
toolUseId?: string
input: unknown
description?: string
}
| {
type: 'computer_use_permission_request'
requestId: string
request: ComputerUsePermissionRequest
}
| { type: 'message_complete'; usage: TokenUsage }
| { type: 'thinking'; text: string }
| { type: 'status'; state: ChatState; verb?: string; elapsed?: number; tokens?: number }
| { type: 'error'; message: string; code: string; retryable?: boolean }
| { type: 'system_notification'; subtype: string; message?: string; data?: unknown }
| { type: 'pong' }
| { type: 'team_update'; teamName: string; members: TeamMemberStatus[] }
| { type: 'team_created'; teamName: string }
| { type: 'team_deleted'; teamName: string }
| { type: 'task_update'; taskId: string; status: string; progress?: string }
| { type: 'session_title_updated'; sessionId: string; title: string }
export type TokenUsage = {
input_tokens: number
output_tokens: number
cache_read_tokens?: number
cache_creation_tokens?: number
}
export type ChatState = 'idle' | 'thinking' | 'tool_executing' | 'streaming' | 'permission_pending'
export type TeamMemberStatus = {
agentId: string
role: string
status: 'running' | 'idle' | 'completed' | 'error'
currentTask?: string
}
export type ComputerUseGrantFlags = {
clipboardRead: boolean
clipboardWrite: boolean
systemKeyCombos: boolean
}
export type ComputerUseResolvedApp = {
bundleId: string
displayName: string
path?: string
iconDataUrl?: string
}
export type ComputerUseResolvedAppRequest = {
requestedName: string
resolved?: ComputerUseResolvedApp
isSentinel: boolean
alreadyGranted: boolean
proposedTier: 'read' | 'click' | 'full'
}
export type ComputerUsePermissionRequest = {
requestId: string
reason: string
apps: ComputerUseResolvedAppRequest[]
requestedFlags: Partial<ComputerUseGrantFlags>
screenshotFiltering: 'native' | 'none'
tccState?: {
accessibility: boolean
screenRecording: boolean
}
willHide?: Array<{ bundleId: string; displayName: string }>
autoUnhideEnabled?: boolean
}
export type ComputerUsePermissionResponse = {
granted: Array<{
bundleId: string
displayName: string
grantedAt: number
tier?: 'read' | 'click' | 'full'
}>
denied: Array<{
bundleId: string
reason: 'user_denied' | 'not_installed'
}>
flags: ComputerUseGrantFlags
userConsented?: boolean
}
// ============================================================================
// Internal types
// ============================================================================
export type WebSocketSession = {
sessionId: string
connectedAt: number
abortController?: AbortController
isGenerating: boolean
}