mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b205f5798 | ||
|
|
7e3d825f0e | ||
|
|
a077ec8d85 | ||
|
|
55a932df68 | ||
|
|
230eb489b5 | ||
|
|
de477aecf6 | ||
|
|
01f26cf42b | ||
|
|
d8892f19d5 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-code-best",
|
||||
"version": "2.6.6",
|
||||
"version": "2.6.11",
|
||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||
"type": "module",
|
||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||
|
||||
@@ -120,6 +120,15 @@ mockModulePreservingExports('../../../utils/listSessionsImpl.ts', {
|
||||
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')
|
||||
|
||||
mockModulePreservingExports('../../../utils/model/model.ts', {
|
||||
@@ -1166,7 +1175,7 @@ describe('AcpAgent', () => {
|
||||
test('newSession calls switchSession with the generated sessionId', async () => {
|
||||
const agent = new AcpAgent(makeConn())
|
||||
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 () => {
|
||||
@@ -1178,7 +1187,10 @@ describe('AcpAgent', () => {
|
||||
mcpServers: [],
|
||||
} as any)
|
||||
|
||||
expect(mockSwitchSession).toHaveBeenCalledWith(requestedId)
|
||||
expect(mockSwitchSession).toHaveBeenCalledWith(
|
||||
requestedId,
|
||||
expect.any(String),
|
||||
)
|
||||
})
|
||||
|
||||
test('loadSession calls switchSession with the requested sessionId', async () => {
|
||||
@@ -1190,7 +1202,10 @@ describe('AcpAgent', () => {
|
||||
mcpServers: [],
|
||||
} as any)
|
||||
|
||||
expect(mockSwitchSession).toHaveBeenCalledWith(requestedId)
|
||||
expect(mockSwitchSession).toHaveBeenCalledWith(
|
||||
requestedId,
|
||||
expect.any(String),
|
||||
)
|
||||
})
|
||||
|
||||
test('resumeSession with existing session still calls switchSession', async () => {
|
||||
@@ -1205,22 +1220,26 @@ describe('AcpAgent', () => {
|
||||
mcpServers: [],
|
||||
} 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())
|
||||
await agent.newSession({ cwd: '/tmp' } as any)
|
||||
await agent.newSession({ cwd: '/tmp' } as any)
|
||||
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
|
||||
await agent.prompt({
|
||||
sessionId: s1,
|
||||
prompt: [{ type: 'text', text: 'hello' }],
|
||||
} as any)
|
||||
expect(mockSwitchSession).not.toHaveBeenCalled()
|
||||
expect(mockSwitchSession).toHaveBeenCalledWith(s1, null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -39,6 +39,7 @@ import type {
|
||||
SessionConfigOption,
|
||||
} from '@agentclientprotocol/sdk'
|
||||
import { randomUUID, type UUID } from 'node:crypto'
|
||||
import { dirname } from 'node:path'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { deserializeMessages } from '../../utils/conversationRecovery.js'
|
||||
import {
|
||||
@@ -53,7 +54,12 @@ import { getEmptyToolPermissionContext } from '../../Tool.js'
|
||||
import type { PermissionMode } from '../../types/permissions.js'
|
||||
import type { Command } from '../../types/command.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 { enableConfigs } from '../../utils/config.js'
|
||||
import { FileStateCache } from '../../utils/fileStateCache.js'
|
||||
@@ -72,6 +78,7 @@ import {
|
||||
} from './utils.js'
|
||||
import { promptToQueryInput } from './promptConversion.js'
|
||||
import { listSessionsImpl } from '../../utils/listSessionsImpl.js'
|
||||
import { resolveSessionFilePath } from '../../utils/sessionStoragePortable.js'
|
||||
import { getMainLoopModel } from '../../utils/model/model.js'
|
||||
import { getModelOptions } from '../../utils/model/modelOptions.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
|
||||
// aborted state — without this, submitMessage() fails immediately.
|
||||
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)
|
||||
|
||||
@@ -474,7 +485,10 @@ export class AcpAgent implements Agent {
|
||||
|
||||
// Align the global session state so that transcript persistence,
|
||||
// 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
|
||||
setOriginalCwd(cwd)
|
||||
@@ -540,8 +554,14 @@ export class AcpAgent implements Agent {
|
||||
},
|
||||
}
|
||||
|
||||
// Load commands for slash command and skill support
|
||||
const commands = await getCommands(cwd)
|
||||
// Load commands and agent definitions for subagent support
|
||||
const [commands, agentDefinitionsResult] = await Promise.all([
|
||||
getCommands(cwd),
|
||||
getAgentDefinitionsWithOverrides(cwd),
|
||||
])
|
||||
|
||||
// Inject agent definitions into appState
|
||||
appState.agentDefinitions = agentDefinitionsResult
|
||||
|
||||
// Build QueryEngine config
|
||||
const engineConfig: QueryEngineConfig = {
|
||||
@@ -549,7 +569,7 @@ export class AcpAgent implements Agent {
|
||||
tools,
|
||||
commands,
|
||||
mcpClients: [],
|
||||
agents: [],
|
||||
agents: agentDefinitionsResult.activeAgents,
|
||||
canUseTool,
|
||||
getAppState: () => appState,
|
||||
setAppState: (updater: (prev: AppState) => AppState) => {
|
||||
@@ -680,8 +700,18 @@ export class AcpAgent implements Agent {
|
||||
| undefined,
|
||||
})
|
||||
if (fingerprint === existingSession.sessionFingerprint) {
|
||||
// Align global state so subsequent operations use the correct session
|
||||
switchSession(params.sessionId as SessionId)
|
||||
const resolved = await resolveSessionFilePath(
|
||||
params.sessionId,
|
||||
params.cwd,
|
||||
)
|
||||
switchSession(
|
||||
params.sessionId as SessionId,
|
||||
resolved ? dirname(resolved.filePath) : null,
|
||||
)
|
||||
setOriginalCwd(params.cwd)
|
||||
|
||||
await this.replaySessionHistory(params)
|
||||
|
||||
return {
|
||||
sessionId: params.sessionId,
|
||||
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)
|
||||
}
|
||||
|
||||
// Align global state BEFORE sessionIdExists() check — the lookup uses
|
||||
// getSessionId() internally when resolving project-scoped paths.
|
||||
switchSession(params.sessionId as SessionId)
|
||||
|
||||
// Set CWD early so session file lookup can find the right project directory
|
||||
// Locate the session file by sessionId across all project directories.
|
||||
// params.cwd may not match the project directory where the session was
|
||||
// originally created (e.g. client sends a subdirectory path), so we
|
||||
// search by sessionId first and fall back to cwd-based lookup.
|
||||
const resolved = await resolveSessionFilePath(params.sessionId, params.cwd)
|
||||
const projectDir = resolved ? dirname(resolved.filePath) : null
|
||||
switchSession(params.sessionId as SessionId, projectDir)
|
||||
setOriginalCwd(params.cwd)
|
||||
|
||||
// Try to load session history for resume/load
|
||||
let initialMessages: Message[] | undefined
|
||||
if (sessionIdExists(params.sessionId)) {
|
||||
if (resolved) {
|
||||
try {
|
||||
const log = await getLastSessionLog(params.sessionId as UUID)
|
||||
if (log && log.messages.length > 0) {
|
||||
@@ -754,6 +784,37 @@ export class AcpAgent implements Agent {
|
||||
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 {
|
||||
if (!isPermissionMode(modeId)) {
|
||||
throw new Error(`Invalid mode: ${modeId}`)
|
||||
|
||||
@@ -633,6 +633,7 @@ export async function forwardSessionUpdates(
|
||||
let lastAssistantTotalUsage: number | null = null
|
||||
let lastAssistantModel: string | null = null
|
||||
let lastContextWindowSize = 200000
|
||||
let streamingActive = false
|
||||
|
||||
try {
|
||||
while (!abortSignal.aborted) {
|
||||
@@ -788,6 +789,7 @@ export async function forwardSessionUpdates(
|
||||
for (const notification of notifications) {
|
||||
await conn.sessionUpdate(notification)
|
||||
}
|
||||
streamingActive = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -827,6 +829,8 @@ export async function forwardSessionUpdates(
|
||||
{
|
||||
clientCapabilities,
|
||||
cwd,
|
||||
parentToolUseId,
|
||||
streamingActive,
|
||||
},
|
||||
)
|
||||
for (const notification of notifications) {
|
||||
@@ -942,6 +946,7 @@ function assistantMessageToAcpNotifications(
|
||||
clientCapabilities?: ClientCapabilities
|
||||
parentToolUseId?: string | null
|
||||
cwd?: string
|
||||
streamingActive?: boolean
|
||||
},
|
||||
): SessionNotification[] {
|
||||
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(
|
||||
content,
|
||||
contentToProcess,
|
||||
'assistant',
|
||||
sessionId,
|
||||
toolUseCache,
|
||||
@@ -987,6 +1004,7 @@ function streamEventToAcpNotifications(
|
||||
options?: {
|
||||
clientCapabilities?: ClientCapabilities
|
||||
cwd?: string
|
||||
streamingActive?: boolean
|
||||
},
|
||||
): SessionNotification[] {
|
||||
const event = (msg as unknown as { event: Record<string, unknown> }).event
|
||||
@@ -1055,6 +1073,7 @@ function toAcpNotifications(
|
||||
clientCapabilities?: ClientCapabilities
|
||||
parentToolUseId?: string | null
|
||||
cwd?: string
|
||||
streamingActive?: boolean
|
||||
},
|
||||
): SessionNotification[] {
|
||||
const output: SessionNotification[] = []
|
||||
|
||||
Reference in New Issue
Block a user