Skip to content
82 changes: 4 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,70 +176,6 @@ http://127.0.0.1:2024

---

## 赞助与合作

本项目由个人利用业余时间维护,欢迎企业或个人赞助支持持续开发,也可洽谈定制、集成或商务合作。

<table>
<thead>
<tr>
<th width="220">赞助商</th>
<th align="left">介绍</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://jiekou.ai/referral?invited_code=OBNU3K">
<img src="docs/images/sponsors/jiekou-logo.svg" width="72" alt="接口AI"><br>
<strong>接口AI</strong>
</a>
</td>
<td valign="middle">
感谢 <a href="https://jiekou.ai/referral?invited_code=OBNU3K">接口AI</a> 赞助本项目!接口AI 提供官方资源直供与稳定高性能 API 体验,订阅包价格为官方 8 折;使用 <a href="https://jiekou.ai/referral?invited_code=OBNU3K">专属链接</a> 注册并绑定 GitHub,可领取 3 美元优惠券。
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://www.shengsuanyun.com/?from=CH_LEJ88KWR">
<img src="docs/images/sponsors/shengsuanyun-logo.svg" width="180" alt="胜算云">
</a>
</td>
<td valign="middle">
感谢 <a href="https://www.shengsuanyun.com/?from=CH_LEJ88KWR">胜算云</a> 赞助本项目!胜算云是面向 AI Native Teams 的工业级 AI 任务并行执行平台,聚合 Claude、ChatGPT、Gemini 等海内外 LLM 及图片、视频多媒体模型算力;官方直连、非逆向,平台 SLA 可用性达 99.7%,可查看 <a href="https://watch.shengsuanyun.com/status/shengsuanyun">服务状态</a>。平台支持企业专属网关、成本与权限管控、智能路由、安全防护和 BYOK,按量与 tokens plan(即将上线)计费并可开票;使用 <a href="https://www.shengsuanyun.com/?from=CH_LEJ88KWR">专属链接</a> 注册可获 10 元模力及首充 10% 赠送。
</td>
</tr>
</tbody>
</table>

📧 **联系邮箱**:relakkes@gmail.com

---

## ☕ 请作者喝杯咖啡

如果这个项目对您有帮助,欢迎打赏支持,您的每一份支持都是我持续更新的动力 ❤️

<table>
<tr>
<td align="center" width="33%">
<img src="docs/images/donate/wechat_pay.jpeg" width="250" alt="微信赞赏"><br>
<b>微信赞赏</b>
</td>
<td align="center" width="33%">
<img src="docs/images/donate/zfb_pay.png" width="250" alt="支付宝"><br>
<b>支付宝</b>
</td>
<td align="center" width="33%">
<a href="https://buymeacoffee.com/relakkes" target="_blank">
<img src="docs/images/donate/bmc_button.png" width="250" alt="Buy Me a Coffee">
</a><br>
<b>Buy Me a Coffee</b>
</td>
</tr>
</table>

---

## 技术栈

Expand Down Expand Up @@ -284,20 +220,10 @@ http://127.0.0.1:2024

---

## ⭐ Star 趋势图

如果这个项目对您有帮助,请给个 ⭐ Star 支持一下,让更多的人看到 Claude Code Haha!

<a href="https://www.star-history.com/#NanmiCoder/cc-haha&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=NanmiCoder/cc-haha&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=NanmiCoder/cc-haha&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=NanmiCoder/cc-haha&type=Date" />
</picture>
</a>
## Disclaimer

---
本仓库基于 [cc-haha](https://github.com/NanmiCoder/cc-haha) 进行二次开发,其中 Claude Code 原始源码的版权归 [Anthropic](https://www.anthropic.com) 所有,该部分仅可用于学习与研究用途。

## Disclaimer
cc-haha 原项目的整体版权归其原作者所有;本仓库**自主修改与新增的代码部分**,允许任何人自由分发、修改与使用。

本仓库基于 2026-03-31 从 Anthropic npm registry 泄露的 Claude Code 源码。所有原始源码版权归 [Anthropic](https://www.anthropic.com) 所有。仅供学习和研究用途。
原项目地址:https://github.com/NanmiCoder/cc-haha
99 changes: 90 additions & 9 deletions desktop/scripts/build-windows-x64.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,44 @@ function Resolve-OutputDirectory {
return $PreferredPath
}

function Remove-PathIfExists {
param([string]$Path)

if (Test-Path -LiteralPath $Path) {
Remove-Item -LiteralPath $Path -Force -Recurse
}
}

function Remove-AppBuildCache {
param([string]$ReleaseDir)

if (-not (Test-Path -LiteralPath $ReleaseDir)) {
return
}

Remove-PathIfExists -Path (Join-Path $ReleaseDir 'bundle')
Remove-PathIfExists -Path (Join-Path $ReleaseDir 'claude-code-desktop.exe')

$buildDir = Join-Path $ReleaseDir 'build'
if (Test-Path -LiteralPath $buildDir) {
Get-ChildItem -LiteralPath $buildDir -Directory -Filter 'claude-code-desktop-*' -ErrorAction SilentlyContinue |
ForEach-Object { Remove-PathIfExists -Path $_.FullName }
}

$fingerprintDir = Join-Path $ReleaseDir '.fingerprint'
if (Test-Path -LiteralPath $fingerprintDir) {
Get-ChildItem -LiteralPath $fingerprintDir -Directory -Filter 'claude-code-desktop-*' -ErrorAction SilentlyContinue |
ForEach-Object { Remove-PathIfExists -Path $_.FullName }
}

$depsDir = Join-Path $ReleaseDir 'deps'
if (Test-Path -LiteralPath $depsDir) {
Get-ChildItem -LiteralPath $depsDir -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like 'claude_code_desktop-*' -or $_.Name -like 'libclaude_code_desktop-*' } |
ForEach-Object { Remove-PathIfExists -Path $_.FullName }
}
}

Assert-WindowsHost
Assert-Command bun

Expand Down Expand Up @@ -189,6 +227,47 @@ if ($env:SKIP_INSTALL -ne '1') {
}
}

Write-Step 'Cleaning stale frontend output, sidecar binaries, and Tauri app cache...'
Get-ChildItem -LiteralPath (Join-Path $desktopDir 'src-tauri\binaries') -Filter 'claude-sidecar-*' -File -ErrorAction SilentlyContinue |
ForEach-Object { Remove-PathIfExists -Path $_.FullName }
Remove-PathIfExists -Path (Join-Path $desktopDir 'dist')
Remove-PathIfExists -Path (Join-Path $desktopDir 'tsconfig.tsbuildinfo')

$targetReleaseDir = Join-Path $tauriTargetDir "$targetTriple\release"
$fallbackReleaseDir = Join-Path $tauriTargetDir 'release'
if ($env:PRESERVE_TAURI_TARGET -eq '1') {
Write-Step 'PRESERVE_TAURI_TARGET=1: keeping Rust dependency cache, clearing app-specific artifacts only...'
Remove-AppBuildCache -ReleaseDir $targetReleaseDir
Remove-AppBuildCache -ReleaseDir $fallbackReleaseDir
} else {
Write-Step "Removing Tauri target cache for $targetTriple to force fresh embedded frontend assets..."
Remove-PathIfExists -Path (Join-Path $tauriTargetDir $targetTriple)
Remove-AppBuildCache -ReleaseDir $fallbackReleaseDir
}

Write-Step 'Rebuilding frontend (tsc + vite)...'
Push-Location $desktopDir
try {
& bun run build
if ($LASTEXITCODE -ne 0) {
throw "[build-windows-x64] bun run build failed (exit $LASTEXITCODE)"
}
} finally {
Pop-Location
}

Write-Step "Rebuilding sidecar for $targetTriple..."
Push-Location $desktopDir
try {
$env:TAURI_ENV_TARGET_TRIPLE = $targetTriple
& bun run build:sidecars
if ($LASTEXITCODE -ne 0) {
throw "[build-windows-x64] bun run build:sidecars failed (exit $LASTEXITCODE)"
}
} finally {
Pop-Location
}

$tauriBuildArgs = @(
'tauri',
'build',
Expand All @@ -199,18 +278,20 @@ $tauriBuildArgs = @(
'--ci'
)

$tempConfigPath = $null
$tempConfigPath = Join-Path ([System.IO.Path]::GetTempPath()) 'cc-haha.tauri.local.windows.json'
$tempConfig = @{
build = @{
beforeBuildCommand = 'cmd /c exit /b 0'
}
}
if (-not $env:TAURI_SIGNING_PRIVATE_KEY) {
$tempConfigPath = Join-Path ([System.IO.Path]::GetTempPath()) 'cc-haha.tauri.local.windows.json'
$tempConfig = @{
bundle = @{
createUpdaterArtifacts = $false
}
} | ConvertTo-Json -Depth 10
Set-Content -Path $tempConfigPath -Value $tempConfig -Encoding UTF8
$tempConfig.bundle = @{
createUpdaterArtifacts = $false
}
Write-Step 'TAURI_SIGNING_PRIVATE_KEY not set, disabling updater artifacts for local build'
$tauriBuildArgs += @('--config', $tempConfigPath)
}
Set-Content -Path $tempConfigPath -Value ($tempConfig | ConvertTo-Json -Depth 10) -Encoding UTF8
$tauriBuildArgs += @('--config', $tempConfigPath)

if ($null -ne $TauriArgs) {
$remainingArgs = @($TauriArgs)
Expand Down
14 changes: 14 additions & 0 deletions desktop/src/__tests__/generalSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,20 @@ describe('Settings > General tab', () => {
})
})

it('saves DuckDuckGo keyless WebSearch mode', () => {
render(<Settings />)

fireEvent.click(screen.getByText('General'))
fireEvent.click(screen.getByRole('button', { name: 'DuckDuckGo' }))
fireEvent.click(screen.getByRole('button', { name: 'Save' }))

expect(useSettingsStore.getState().setWebSearch).toHaveBeenCalledWith({
mode: 'duckduckgo',
tavilyApiKey: '',
braveApiKey: '',
})
})

it('links to WebSearch provider API key dashboards', () => {
render(<Settings />)

Expand Down
2 changes: 1 addition & 1 deletion desktop/src/__tests__/pages.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ describe('Content-only pages render without errors', () => {
expect(screen.getByText('Slash commands')).toBeInTheDocument()
expect(screen.getByText('/clear')).toBeInTheDocument()
expect(screen.getByText('/cost')).toBeInTheDocument()
expect(screen.getByText('13 more commands available. Type / to search the full command list.')).toBeInTheDocument()
expect(screen.getByText('15 more commands available. Type / to search the full command list.')).toBeInTheDocument()

resetPageStores()
})
Expand Down
64 changes: 64 additions & 0 deletions desktop/src/components/chat/ChatInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -428,4 +428,68 @@ describe('ChatInput file mentions', () => {
attachments: [{ name: 'conditions.py', path: '/repo/backend/src/conditions.py' }],
})
})

it('restores recalled file attachments into the composer', async () => {
useChatStore.setState({
sessions: {
[sessionId]: {
messages: [{ id: 'existing', type: 'assistant_text', content: 'ready', timestamp: 1 }],
chatState: 'idle',
connectionState: 'connected',
streamingText: '',
streamingToolInput: '',
activeToolUseId: null,
activeToolName: null,
activeThinkingId: null,
pendingPermission: null,
pendingComputerUsePermission: null,
tokenUsage: { input_tokens: 0, output_tokens: 0 },
elapsedSeconds: 0,
statusVerb: '',
slashCommands: [],
agentTaskNotifications: {},
elapsedTimer: null,
composerPrefill: {
text: 'Update this section',
nonce: 1,
attachments: [{
type: 'file',
name: 'App.tsx',
path: 'src/App.tsx',
lineStart: 12,
lineEnd: 18,
note: 'tighten copy',
quote: '<Header />',
}],
},
},
},
})

render(<ChatInput compact />)

const input = screen.getByRole('textbox') as HTMLTextAreaElement
await waitFor(() => {
expect(input.value).toBe('Update this section')
})
expect(screen.getByRole('button', { name: 'Remove App.tsx' })).toBeInTheDocument()

fireEvent.keyDown(input, { key: 'Enter' })

expect(mocks.wsSend).toHaveBeenCalledWith(sessionId, {
type: 'user_message',
content: 'Update this section',
attachments: [{
type: 'file',
name: 'App.tsx',
path: 'src/App.tsx',
data: undefined,
mimeType: undefined,
lineStart: 12,
lineEnd: 18,
note: 'tighten copy',
quote: '<Header />',
}],
})
})
})
7 changes: 6 additions & 1 deletion desktop/src/components/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,19 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
setInput(composerPrefill.text)
setAttachments(
(composerPrefill.attachments ?? [])
.filter((attachment) => attachment.type === 'image' || attachment.data)
.filter((attachment) => attachment.type === 'image' || attachment.data || attachment.path)
.map((attachment, index) => ({
id: `rewind-prefill-${composerPrefill.nonce}-${index}`,
name: attachment.name,
type: attachment.type,
path: attachment.path,
mimeType: attachment.mimeType,
previewUrl: attachment.type === 'image' ? attachment.data : undefined,
data: attachment.data,
lineStart: attachment.lineStart,
lineEnd: attachment.lineEnd,
note: attachment.note,
quote: attachment.quote,
})),
)
setPlusMenuOpen(false)
Expand Down
Loading
Loading