Skip to content

Commit b9cf396

Browse files
authored
Merge branch 'claude-code-best:main' into main
2 parents 3ca88e4 + 4cbf406 commit b9cf396

30 files changed

Lines changed: 718 additions & 126 deletions

CLAUDE.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,8 @@ bun run health
6060
# Check unused exports
6161
bun run check:unused
6262

63-
# Full check (typecheck + lint + test) — run after completing any task
64-
bun run test:all
65-
bun run typecheck
63+
# Full check (typecheck + lint fix + test) — run after completing any task
64+
bun run precheck
6665

6766
# Remote Control Server
6867
bun run rcs

docs/memory-peak-analysis.md

Lines changed: 294 additions & 0 deletions
Large diffs are not rendered by default.

docs/tools/file-operations.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ Claude Code 将文件操作拆分为三个独立工具——这不是功能划
1212

1313
| 工具 | 权限级别 | 核心方法 | 关键属性 |
1414
|------|---------|---------|---------|
15-
| **Read** | 只读(免审批) | `isReadOnly() → true` | `maxResultSizeChars: Infinity` |
15+
| **Read** | 只读(免审批) | `isReadOnly() → true` | `maxResultSizeChars: 100,000` |
1616
| **Edit** | 写入(需确认) | `checkWritePermissionForTool()` | `maxResultSizeChars: 100,000` |
1717
| **Write** | 写入(需确认) | `checkWritePermissionForTool()` | `maxResultSizeChars: 100,000` |
1818

1919
<Tip>
20-
Read 的 `maxResultSizeChars` `Infinity`,但这并不意味着无限制输出——真正的截断发生在 `validateContentTokens()` 中基于 token 预算的动态判定,而非字符数硬限制
20+
Read 的 `maxResultSizeChars` 为 100,000(100KB)。超出此阈值的结果会被持久化到磁盘,减少长会话的内存压力。实际的 token 级别截断由 `validateContentTokens()` 动态控制
2121
</Tip>
2222

2323
## FileRead:多模态文件读取引擎

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claude-code-best",
3-
"version": "1.11.1",
3+
"version": "2.0.1",
44
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
55
"type": "module",
66
"author": "claude-code-best <claude-code-best@proton.me>",
@@ -65,7 +65,7 @@
6565
"postinstall": "node scripts/run-parallel.mjs scripts/postinstall.cjs scripts/setup-chrome-mcp.mjs",
6666
"docs:dev": "npx mintlify dev",
6767
"typecheck": "tsc --noEmit",
68-
"test:all": "bun run typecheck && bun test",
68+
"precheck": "bun run typecheck && bun run check:fix && bun test",
6969
"rcs": "bun run scripts/rcs.ts"
7070
},
7171
"dependencies": {

packages/@ant/ink/src/core/screen.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,44 @@ export class StylePool {
119119
this.none = this.intern([])
120120
}
121121

122+
private static readonly CACHE_MAX = 1000
123+
124+
/**
125+
* Evict oldest entries from derivative caches when they exceed the limit.
126+
* ids/styles are never evicted (id is an array index).
127+
*/
128+
private evictCacheIfNeeded(): void {
129+
if (this.transitionCache.size > StylePool.CACHE_MAX) {
130+
const keys = this.transitionCache.keys()
131+
for (
132+
let i = 0;
133+
i < this.transitionCache.size - StylePool.CACHE_MAX;
134+
i++
135+
) {
136+
const k = keys.next().value
137+
if (k !== undefined) this.transitionCache.delete(k)
138+
}
139+
}
140+
if (this.inverseCache.size > StylePool.CACHE_MAX) {
141+
const keys = this.inverseCache.keys()
142+
for (let i = 0; i < this.inverseCache.size - StylePool.CACHE_MAX; i++) {
143+
const k = keys.next().value
144+
if (k !== undefined) this.inverseCache.delete(k)
145+
}
146+
}
147+
if (this.currentMatchCache.size > StylePool.CACHE_MAX) {
148+
const keys = this.currentMatchCache.keys()
149+
for (
150+
let i = 0;
151+
i < this.currentMatchCache.size - StylePool.CACHE_MAX;
152+
i++
153+
) {
154+
const k = keys.next().value
155+
if (k !== undefined) this.currentMatchCache.delete(k)
156+
}
157+
}
158+
}
159+
122160
/**
123161
* Intern a style and return its ID. Bit 0 of the ID encodes whether the
124162
* style has a visible effect on space characters (background, inverse,
@@ -136,6 +174,7 @@ export class StylePool {
136174
(rawId << 1) |
137175
(styles.length > 0 && hasVisibleSpaceEffect(styles) ? 1 : 0)
138176
this.ids.set(key, id)
177+
this.evictCacheIfNeeded()
139178
}
140179
return id
141180
}

packages/@ant/model-provider/src/shared/__tests__/openaiConvertMessages.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,11 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
468468
expect(assistant.reasoning_content).toBe('First thought.\nSecond thought.')
469469
})
470470

471-
test('skips empty thinking blocks', () => {
471+
test('preserves empty thinking blocks as reasoning_content: "" (DeepSeek v4 thinking mode)', () => {
472+
// DeepSeek v4 thinking mode sometimes returns reasoning_content: ""
473+
// when the model answers directly without reasoning. The empty value
474+
// must be echoed back in the next request — otherwise DeepSeek returns
475+
// 400 ("reasoning_content ... must be passed back"). See issue #399.
472476
const result = anthropicMessagesToOpenAI(
473477
[
474478
makeUserMsg('question'),
@@ -481,7 +485,23 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
481485
{ enableThinking: true },
482486
)
483487
const assistant = result.filter(m => m.role === 'assistant')[0] as any
488+
expect(assistant.reasoning_content).toBe('')
489+
expect(assistant.content).toBe('Answer.')
490+
})
491+
492+
test('omits reasoning_content when no thinking block is present', () => {
493+
// No thinking block at all → no reasoning_content field on the
494+
// OpenAI-format assistant message (relevant for non-thinking models).
495+
const result = anthropicMessagesToOpenAI(
496+
[
497+
makeUserMsg('question'),
498+
makeAssistantMsg([{ type: 'text', text: 'Answer.' }]),
499+
],
500+
[] as any,
501+
)
502+
const assistant = result.filter(m => m.role === 'assistant')[0] as any
484503
expect(assistant.reasoning_content).toBeUndefined()
504+
expect(assistant.content).toBe('Answer.')
485505
})
486506

487507
// ── fix: reorder tool and user messages for OpenAI API compatibility (#168) ──

packages/@ant/model-provider/src/shared/__tests__/openaiStreamAdapter.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,54 @@ describe('thinking support (reasoning_content)', () => {
439439
expect(blockStarts[1].content_block.type).toBe('tool_use')
440440
})
441441

442+
test('opens thinking block on empty reasoning_content (DeepSeek v4 direct-answer)', async () => {
443+
// DeepSeek v4 thinking mode sometimes streams reasoning_content: ""
444+
// before answering directly. We must still open a thinking block so the
445+
// resulting assistant message carries an (empty) thinking block — that
446+
// round-trips back as reasoning_content: "" in the next request,
447+
// satisfying DeepSeek's requirement (see issue #399).
448+
const events = await collectEvents([
449+
makeChunk({
450+
choices: [
451+
{
452+
index: 0,
453+
delta: { reasoning_content: '' },
454+
finish_reason: null,
455+
},
456+
],
457+
}),
458+
makeChunk({
459+
choices: [
460+
{
461+
index: 0,
462+
delta: { content: 'Direct answer.' },
463+
finish_reason: null,
464+
},
465+
],
466+
}),
467+
makeChunk({
468+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
469+
}),
470+
])
471+
472+
// A thinking block was opened (and closed before the text block starts)
473+
const blockStarts = events.filter(
474+
e => e.type === 'content_block_start',
475+
) as any[]
476+
expect(blockStarts.length).toBe(2)
477+
expect(blockStarts[0].content_block.type).toBe('thinking')
478+
expect(blockStarts[0].content_block.thinking).toBe('')
479+
expect(blockStarts[1].content_block.type).toBe('text')
480+
481+
// No empty thinking_delta should be emitted — the empty string is
482+
// already conveyed by the thinking block's initial value.
483+
const thinkingDeltas = events.filter(
484+
e =>
485+
e.type === 'content_block_delta' && e.delta.type === 'thinking_delta',
486+
)
487+
expect(thinkingDeltas.length).toBe(0)
488+
})
489+
442490
test('thinking block index is 0, text block index is 1', async () => {
443491
const events = await collectEvents([
444492
makeChunk({

packages/@ant/model-provider/src/shared/openaiConvertMessages.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,14 @@ function convertInternalAssistantMessage(
206206
},
207207
})
208208
} else if (block.type === 'thinking') {
209-
// DeepSeek thinking mode: always preserve reasoning_content.
210-
// DeepSeek requires reasoning_content to be passed back in subsequent requests,
211-
// especially when tool calls are involved (returns 400 if missing).
209+
// DeepSeek thinking mode: always preserve reasoning_content,
210+
// including the empty-string case. DeepSeek v4 may return
211+
// reasoning_content: "" when the model answers directly, and the
212+
// empty value must be echoed back in the next request — otherwise
213+
// DeepSeek returns 400 ("reasoning_content ... must be passed back").
212214
const thinkingText = (block as unknown as Record<string, unknown>)
213215
.thinking
214-
if (typeof thinkingText === 'string' && thinkingText) {
216+
if (typeof thinkingText === 'string') {
215217
reasoningParts.push(thinkingText)
216218
}
217219
}

packages/@ant/model-provider/src/shared/openaiStreamAdapter.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,13 @@ export async function* adaptOpenAIStreamToAnthropic(
106106
// Skip chunks that carry only usage data (no delta content)
107107
if (!delta) continue
108108

109-
// Handle reasoning_content → Anthropic thinking block
109+
// Handle reasoning_content → Anthropic thinking block.
110+
// Empty string is a valid signal: DeepSeek v4 thinking mode sometimes
111+
// returns reasoning_content: "" when the model answers directly. The
112+
// empty thinking block must round-trip back to the API in subsequent
113+
// requests, otherwise DeepSeek rejects with 400.
110114
const reasoningContent = (delta as any).reasoning_content
111-
if (reasoningContent != null && reasoningContent !== '') {
115+
if (reasoningContent != null) {
112116
if (!thinkingBlockOpen) {
113117
currentContentIndex++
114118
thinkingBlockOpen = true
@@ -125,14 +129,16 @@ export async function* adaptOpenAIStreamToAnthropic(
125129
} as BetaRawMessageStreamEvent
126130
}
127131

128-
yield {
129-
type: 'content_block_delta',
130-
index: currentContentIndex,
131-
delta: {
132-
type: 'thinking_delta',
133-
thinking: reasoningContent,
134-
},
135-
} as BetaRawMessageStreamEvent
132+
if (reasoningContent !== '') {
133+
yield {
134+
type: 'content_block_delta',
135+
index: currentContentIndex,
136+
delta: {
137+
type: 'thinking_delta',
138+
thinking: reasoningContent,
139+
},
140+
} as BetaRawMessageStreamEvent
141+
}
136142
}
137143

138144
// Handle text content

packages/builtin-tools/src/tools/FileReadTool/FileReadTool.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,10 @@ export type Output = z.infer<OutputSchema>
337337
export const FileReadTool = buildTool({
338338
name: FILE_READ_TOOL_NAME,
339339
searchHint: 'read files, images, PDFs, notebooks',
340-
// Output is bounded by maxTokens (validateContentTokens). Persisting to a
341-
// file the model reads back with Read is circular — never persist.
342-
maxResultSizeChars: Infinity,
340+
// Output is bounded by maxTokens (validateContentTokens). Results exceeding
341+
// 100KB are persisted to disk (reducing memory pressure in long sessions)
342+
// rather than kept in the message array indefinitely.
343+
maxResultSizeChars: 100_000,
343344
strict: true,
344345
async description() {
345346
return DESCRIPTION

0 commit comments

Comments
 (0)