mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b205f5798 | ||
|
|
7e3d825f0e | ||
|
|
a077ec8d85 | ||
|
|
55a932df68 | ||
|
|
230eb489b5 | ||
|
|
de477aecf6 | ||
|
|
01f26cf42b |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"version": "2.6.8",
|
"version": "2.6.11",
|
||||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
|
|||||||
@@ -120,6 +120,15 @@ mockModulePreservingExports('../../../utils/listSessionsImpl.ts', {
|
|||||||
listSessionsImpl: mock(async () => []),
|
listSessionsImpl: mock(async () => []),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const mockResolveSessionFilePath = mock(async () => ({
|
||||||
|
filePath: '/fake/project/dir/session.jsonl',
|
||||||
|
projectPath: '/tmp',
|
||||||
|
fileSize: 100,
|
||||||
|
}))
|
||||||
|
mockModulePreservingExports('../../../utils/sessionStoragePortable.js', {
|
||||||
|
resolveSessionFilePath: mockResolveSessionFilePath,
|
||||||
|
})
|
||||||
|
|
||||||
const mockGetMainLoopModel = mock(() => 'claude-sonnet-4-6')
|
const mockGetMainLoopModel = mock(() => 'claude-sonnet-4-6')
|
||||||
|
|
||||||
mockModulePreservingExports('../../../utils/model/model.ts', {
|
mockModulePreservingExports('../../../utils/model/model.ts', {
|
||||||
@@ -1166,7 +1175,7 @@ describe('AcpAgent', () => {
|
|||||||
test('newSession calls switchSession with the generated sessionId', async () => {
|
test('newSession calls switchSession with the generated sessionId', async () => {
|
||||||
const agent = new AcpAgent(makeConn())
|
const agent = new AcpAgent(makeConn())
|
||||||
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
expect(mockSwitchSession).toHaveBeenCalledWith(res.sessionId)
|
expect(mockSwitchSession).toHaveBeenCalledWith(res.sessionId, null)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resumeSession calls switchSession with the requested sessionId', async () => {
|
test('resumeSession calls switchSession with the requested sessionId', async () => {
|
||||||
@@ -1178,7 +1187,10 @@ describe('AcpAgent', () => {
|
|||||||
mcpServers: [],
|
mcpServers: [],
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
expect(mockSwitchSession).toHaveBeenCalledWith(requestedId)
|
expect(mockSwitchSession).toHaveBeenCalledWith(
|
||||||
|
requestedId,
|
||||||
|
expect.any(String),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('loadSession calls switchSession with the requested sessionId', async () => {
|
test('loadSession calls switchSession with the requested sessionId', async () => {
|
||||||
@@ -1190,7 +1202,10 @@ describe('AcpAgent', () => {
|
|||||||
mcpServers: [],
|
mcpServers: [],
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
expect(mockSwitchSession).toHaveBeenCalledWith(requestedId)
|
expect(mockSwitchSession).toHaveBeenCalledWith(
|
||||||
|
requestedId,
|
||||||
|
expect.any(String),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resumeSession with existing session still calls switchSession', async () => {
|
test('resumeSession with existing session still calls switchSession', async () => {
|
||||||
@@ -1205,22 +1220,26 @@ describe('AcpAgent', () => {
|
|||||||
mcpServers: [],
|
mcpServers: [],
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
expect(mockSwitchSession).toHaveBeenCalledWith(sessionId)
|
expect(mockSwitchSession).toHaveBeenCalledWith(
|
||||||
|
sessionId,
|
||||||
|
expect.any(String),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prompt does not trigger additional switchSession for multi-session', async () => {
|
test('prompt switches global sessionId to the correct session', async () => {
|
||||||
const agent = new AcpAgent(makeConn())
|
const agent = new AcpAgent(makeConn())
|
||||||
await agent.newSession({ cwd: '/tmp' } as any)
|
await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
await agent.newSession({ cwd: '/tmp' } as any)
|
await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
mockSwitchSession.mockClear()
|
mockSwitchSession.mockClear()
|
||||||
|
|
||||||
// Prompts should not call switchSession — alignment happens at session creation
|
// Prompts must switch global state so recordTranscript writes to
|
||||||
|
// the correct session file in multi-session scenarios.
|
||||||
const s1 = agent.sessions.keys().next().value
|
const s1 = agent.sessions.keys().next().value
|
||||||
await agent.prompt({
|
await agent.prompt({
|
||||||
sessionId: s1,
|
sessionId: s1,
|
||||||
prompt: [{ type: 'text', text: 'hello' }],
|
prompt: [{ type: 'text', text: 'hello' }],
|
||||||
} as any)
|
} as any)
|
||||||
expect(mockSwitchSession).not.toHaveBeenCalled()
|
expect(mockSwitchSession).toHaveBeenCalledWith(s1, null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import type {
|
|||||||
SessionConfigOption,
|
SessionConfigOption,
|
||||||
} from '@agentclientprotocol/sdk'
|
} from '@agentclientprotocol/sdk'
|
||||||
import { randomUUID, type UUID } from 'node:crypto'
|
import { randomUUID, type UUID } from 'node:crypto'
|
||||||
|
import { dirname } from 'node:path'
|
||||||
import type { Message } from '../../types/message.js'
|
import type { Message } from '../../types/message.js'
|
||||||
import { deserializeMessages } from '../../utils/conversationRecovery.js'
|
import { deserializeMessages } from '../../utils/conversationRecovery.js'
|
||||||
import {
|
import {
|
||||||
@@ -53,7 +54,12 @@ import { getEmptyToolPermissionContext } from '../../Tool.js'
|
|||||||
import type { PermissionMode } from '../../types/permissions.js'
|
import type { PermissionMode } from '../../types/permissions.js'
|
||||||
import type { Command } from '../../types/command.js'
|
import type { Command } from '../../types/command.js'
|
||||||
import { getCommands } from '../../commands.js'
|
import { getCommands } from '../../commands.js'
|
||||||
import { setOriginalCwd, switchSession } from '../../bootstrap/state.js'
|
import { getAgentDefinitionsWithOverrides } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
|
||||||
|
import {
|
||||||
|
setOriginalCwd,
|
||||||
|
switchSession,
|
||||||
|
getSessionProjectDir,
|
||||||
|
} from '../../bootstrap/state.js'
|
||||||
import type { SessionId } from '../../types/ids.js'
|
import type { SessionId } from '../../types/ids.js'
|
||||||
import { enableConfigs } from '../../utils/config.js'
|
import { enableConfigs } from '../../utils/config.js'
|
||||||
import { FileStateCache } from '../../utils/fileStateCache.js'
|
import { FileStateCache } from '../../utils/fileStateCache.js'
|
||||||
@@ -72,6 +78,7 @@ import {
|
|||||||
} from './utils.js'
|
} from './utils.js'
|
||||||
import { promptToQueryInput } from './promptConversion.js'
|
import { promptToQueryInput } from './promptConversion.js'
|
||||||
import { listSessionsImpl } from '../../utils/listSessionsImpl.js'
|
import { listSessionsImpl } from '../../utils/listSessionsImpl.js'
|
||||||
|
import { resolveSessionFilePath } from '../../utils/sessionStoragePortable.js'
|
||||||
import { getMainLoopModel } from '../../utils/model/model.js'
|
import { getMainLoopModel } from '../../utils/model/model.js'
|
||||||
import { getModelOptions } from '../../utils/model/modelOptions.js'
|
import { getModelOptions } from '../../utils/model/modelOptions.js'
|
||||||
import { getSettings_DEPRECATED } from '../../utils/settings/settings.js'
|
import { getSettings_DEPRECATED } from '../../utils/settings/settings.js'
|
||||||
@@ -293,6 +300,10 @@ export class AcpAgent implements Agent {
|
|||||||
// After a previous interrupt(), the internal controller is stuck in
|
// After a previous interrupt(), the internal controller is stuck in
|
||||||
// aborted state — without this, submitMessage() fails immediately.
|
// aborted state — without this, submitMessage() fails immediately.
|
||||||
session.queryEngine.resetAbortController()
|
session.queryEngine.resetAbortController()
|
||||||
|
// Switch global session state so recordTranscript writes to the correct
|
||||||
|
// session file. Without this, multi-session scenarios (or creating a new
|
||||||
|
// session after another) write transcript data to the wrong file.
|
||||||
|
switchSession(params.sessionId as SessionId, getSessionProjectDir())
|
||||||
|
|
||||||
const sdkMessages = session.queryEngine.submitMessage(promptInput)
|
const sdkMessages = session.queryEngine.submitMessage(promptInput)
|
||||||
|
|
||||||
@@ -474,7 +485,10 @@ export class AcpAgent implements Agent {
|
|||||||
|
|
||||||
// Align the global session state so that transcript persistence,
|
// Align the global session state so that transcript persistence,
|
||||||
// analytics, and cost tracking use the ACP session ID.
|
// analytics, and cost tracking use the ACP session ID.
|
||||||
switchSession(sessionId as SessionId)
|
// Preserve the projectDir set by getOrCreateSession so that
|
||||||
|
// getSessionProjectDir() continues to resolve correctly.
|
||||||
|
const currentProjectDir = getSessionProjectDir()
|
||||||
|
switchSession(sessionId as SessionId, currentProjectDir)
|
||||||
|
|
||||||
// Set CWD for the session
|
// Set CWD for the session
|
||||||
setOriginalCwd(cwd)
|
setOriginalCwd(cwd)
|
||||||
@@ -540,8 +554,14 @@ export class AcpAgent implements Agent {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load commands for slash command and skill support
|
// Load commands and agent definitions for subagent support
|
||||||
const commands = await getCommands(cwd)
|
const [commands, agentDefinitionsResult] = await Promise.all([
|
||||||
|
getCommands(cwd),
|
||||||
|
getAgentDefinitionsWithOverrides(cwd),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Inject agent definitions into appState
|
||||||
|
appState.agentDefinitions = agentDefinitionsResult
|
||||||
|
|
||||||
// Build QueryEngine config
|
// Build QueryEngine config
|
||||||
const engineConfig: QueryEngineConfig = {
|
const engineConfig: QueryEngineConfig = {
|
||||||
@@ -549,7 +569,7 @@ export class AcpAgent implements Agent {
|
|||||||
tools,
|
tools,
|
||||||
commands,
|
commands,
|
||||||
mcpClients: [],
|
mcpClients: [],
|
||||||
agents: [],
|
agents: agentDefinitionsResult.activeAgents,
|
||||||
canUseTool,
|
canUseTool,
|
||||||
getAppState: () => appState,
|
getAppState: () => appState,
|
||||||
setAppState: (updater: (prev: AppState) => AppState) => {
|
setAppState: (updater: (prev: AppState) => AppState) => {
|
||||||
@@ -680,8 +700,18 @@ export class AcpAgent implements Agent {
|
|||||||
| undefined,
|
| undefined,
|
||||||
})
|
})
|
||||||
if (fingerprint === existingSession.sessionFingerprint) {
|
if (fingerprint === existingSession.sessionFingerprint) {
|
||||||
// Align global state so subsequent operations use the correct session
|
const resolved = await resolveSessionFilePath(
|
||||||
switchSession(params.sessionId as SessionId)
|
params.sessionId,
|
||||||
|
params.cwd,
|
||||||
|
)
|
||||||
|
switchSession(
|
||||||
|
params.sessionId as SessionId,
|
||||||
|
resolved ? dirname(resolved.filePath) : null,
|
||||||
|
)
|
||||||
|
setOriginalCwd(params.cwd)
|
||||||
|
|
||||||
|
await this.replaySessionHistory(params)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
modes: existingSession.modes,
|
modes: existingSession.modes,
|
||||||
@@ -690,20 +720,20 @@ export class AcpAgent implements Agent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session-defining params changed — tear down and recreate
|
|
||||||
await this.teardownSession(params.sessionId)
|
await this.teardownSession(params.sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Align global state BEFORE sessionIdExists() check — the lookup uses
|
// Locate the session file by sessionId across all project directories.
|
||||||
// getSessionId() internally when resolving project-scoped paths.
|
// params.cwd may not match the project directory where the session was
|
||||||
switchSession(params.sessionId as SessionId)
|
// originally created (e.g. client sends a subdirectory path), so we
|
||||||
|
// search by sessionId first and fall back to cwd-based lookup.
|
||||||
// Set CWD early so session file lookup can find the right project directory
|
const resolved = await resolveSessionFilePath(params.sessionId, params.cwd)
|
||||||
|
const projectDir = resolved ? dirname(resolved.filePath) : null
|
||||||
|
switchSession(params.sessionId as SessionId, projectDir)
|
||||||
setOriginalCwd(params.cwd)
|
setOriginalCwd(params.cwd)
|
||||||
|
|
||||||
// Try to load session history for resume/load
|
|
||||||
let initialMessages: Message[] | undefined
|
let initialMessages: Message[] | undefined
|
||||||
if (sessionIdExists(params.sessionId)) {
|
if (resolved) {
|
||||||
try {
|
try {
|
||||||
const log = await getLastSessionLog(params.sessionId as UUID)
|
const log = await getLastSessionLog(params.sessionId as UUID)
|
||||||
if (log && log.messages.length > 0) {
|
if (log && log.messages.length > 0) {
|
||||||
@@ -754,6 +784,37 @@ export class AcpAgent implements Agent {
|
|||||||
this.sessions.delete(sessionId)
|
this.sessions.delete(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load session history from disk and replay it to the ACP client.
|
||||||
|
* Used when switching back to a session that is already in memory
|
||||||
|
* (the client needs the conversation replayed to display it).
|
||||||
|
*/
|
||||||
|
private async replaySessionHistory(params: {
|
||||||
|
sessionId: string
|
||||||
|
cwd: string
|
||||||
|
}): Promise<void> {
|
||||||
|
try {
|
||||||
|
const log = await getLastSessionLog(params.sessionId as UUID)
|
||||||
|
if (!log || log.messages.length === 0) return
|
||||||
|
const messages = deserializeMessages(log.messages)
|
||||||
|
if (messages.length === 0) return
|
||||||
|
|
||||||
|
const session = this.sessions.get(params.sessionId)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
await replayHistoryMessages(
|
||||||
|
params.sessionId,
|
||||||
|
messages as unknown as Array<Record<string, unknown>>,
|
||||||
|
this.conn,
|
||||||
|
session.toolUseCache,
|
||||||
|
this.clientCapabilities,
|
||||||
|
session.cwd,
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ACP] Failed to replay session history:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private applySessionMode(sessionId: string, modeId: string): void {
|
private applySessionMode(sessionId: string, modeId: string): void {
|
||||||
if (!isPermissionMode(modeId)) {
|
if (!isPermissionMode(modeId)) {
|
||||||
throw new Error(`Invalid mode: ${modeId}`)
|
throw new Error(`Invalid mode: ${modeId}`)
|
||||||
|
|||||||
@@ -633,6 +633,7 @@ export async function forwardSessionUpdates(
|
|||||||
let lastAssistantTotalUsage: number | null = null
|
let lastAssistantTotalUsage: number | null = null
|
||||||
let lastAssistantModel: string | null = null
|
let lastAssistantModel: string | null = null
|
||||||
let lastContextWindowSize = 200000
|
let lastContextWindowSize = 200000
|
||||||
|
let streamingActive = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (!abortSignal.aborted) {
|
while (!abortSignal.aborted) {
|
||||||
@@ -788,6 +789,7 @@ export async function forwardSessionUpdates(
|
|||||||
for (const notification of notifications) {
|
for (const notification of notifications) {
|
||||||
await conn.sessionUpdate(notification)
|
await conn.sessionUpdate(notification)
|
||||||
}
|
}
|
||||||
|
streamingActive = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,6 +829,8 @@ export async function forwardSessionUpdates(
|
|||||||
{
|
{
|
||||||
clientCapabilities,
|
clientCapabilities,
|
||||||
cwd,
|
cwd,
|
||||||
|
parentToolUseId,
|
||||||
|
streamingActive,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
for (const notification of notifications) {
|
for (const notification of notifications) {
|
||||||
@@ -942,6 +946,7 @@ function assistantMessageToAcpNotifications(
|
|||||||
clientCapabilities?: ClientCapabilities
|
clientCapabilities?: ClientCapabilities
|
||||||
parentToolUseId?: string | null
|
parentToolUseId?: string | null
|
||||||
cwd?: string
|
cwd?: string
|
||||||
|
streamingActive?: boolean
|
||||||
},
|
},
|
||||||
): SessionNotification[] {
|
): SessionNotification[] {
|
||||||
const message = msg.message as Record<string, unknown> | undefined
|
const message = msg.message as Record<string, unknown> | undefined
|
||||||
@@ -966,8 +971,20 @@ function assistantMessageToAcpNotifications(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When streaming is active, text/thinking were already sent via stream_event
|
||||||
|
// messages. Filter them out to avoid duplicate agent_message_chunk /
|
||||||
|
// agent_thought_chunk notifications. String content (synthetic messages)
|
||||||
|
// is unaffected — those have no corresponding stream_events.
|
||||||
|
const contentToProcess = options?.streamingActive
|
||||||
|
? content.filter(
|
||||||
|
block => block.type !== 'text' && block.type !== 'thinking',
|
||||||
|
)
|
||||||
|
: content
|
||||||
|
|
||||||
|
if (contentToProcess.length === 0) return []
|
||||||
|
|
||||||
return toAcpNotifications(
|
return toAcpNotifications(
|
||||||
content,
|
contentToProcess,
|
||||||
'assistant',
|
'assistant',
|
||||||
sessionId,
|
sessionId,
|
||||||
toolUseCache,
|
toolUseCache,
|
||||||
@@ -987,6 +1004,7 @@ function streamEventToAcpNotifications(
|
|||||||
options?: {
|
options?: {
|
||||||
clientCapabilities?: ClientCapabilities
|
clientCapabilities?: ClientCapabilities
|
||||||
cwd?: string
|
cwd?: string
|
||||||
|
streamingActive?: boolean
|
||||||
},
|
},
|
||||||
): SessionNotification[] {
|
): SessionNotification[] {
|
||||||
const event = (msg as unknown as { event: Record<string, unknown> }).event
|
const event = (msg as unknown as { event: Record<string, unknown> }).event
|
||||||
@@ -1055,6 +1073,7 @@ function toAcpNotifications(
|
|||||||
clientCapabilities?: ClientCapabilities
|
clientCapabilities?: ClientCapabilities
|
||||||
parentToolUseId?: string | null
|
parentToolUseId?: string | null
|
||||||
cwd?: string
|
cwd?: string
|
||||||
|
streamingActive?: boolean
|
||||||
},
|
},
|
||||||
): SessionNotification[] {
|
): SessionNotification[] {
|
||||||
const output: SessionNotification[] = []
|
const output: SessionNotification[] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user