Feat/integrate lint preview (#285)

* feat: 适配 zed acp 协议

* docs: 完善 acp 文档

* feat: integrate feature branches + daemon/job 命令层级化 + 跨平台后台引擎

Cherry-picked from origin/lint/preview (637c908), excluding lint-only changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct detectMimeFromBase64 to decode raw bytes from base64

Cherry-picked from origin/lint/preview (ee36954).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: daemon 子进程 spawn 跨平台修复 + CliLaunchSpec 集中化重构

Cherry-picked from origin/lint/preview (c5f52cd), excluding lint-only formatting changes.

- 新建 src/utils/cliLaunch.ts: 集中化 CLI 子进程启动层
- 修复 --daemon-worker=kind 等号格式解析
- 修复 daemon/bg fast path 缺少 setShellIfWindows()
- 修复 checkPathExists 用 existsSync 替代 execSync('dir')
- 7 个 spawn 站点迁移到 CliLaunchSpec

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge tsconfig.base.json into tsconfig.json with full compiler options

The cherry-pick from 637c908 dropped jsx/strict/etc settings when removing
tsconfig.base.json. This commit restores them in a single tsconfig.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge tsconfig.base.json into tsconfig.json with full compiler options

The cherry-pick from 637c908 dropped jsx/strict/etc settings when removing
tsconfig.base.json. This commit restores them in a single tsconfig.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-16 20:59:29 +08:00
committed by GitHub
parent a02dc0bded
commit c8d08d235b
137 changed files with 13267 additions and 837 deletions

View File

@@ -26,6 +26,12 @@ import { fileHistoryEnabled, fileHistoryMakeSnapshot } from './fileHistory.js'
import { gracefulShutdownSync } from './gracefulShutdown.js'
import { enqueue } from './messageQueueManager.js'
import { resolveSkillModelOverride } from './model/model.js'
import {
finalizeAutonomyRunCompleted,
finalizeAutonomyRunFailed,
markAutonomyRunFailed,
markAutonomyRunRunning,
} from './autonomyRuns.js'
import type { ProcessUserInputContext } from './processUserInput/processUserInput.js'
import { processUserInput } from './processUserInput/processUserInput.js'
import type { QueryGuard } from './QueryGuard.js'
@@ -460,6 +466,7 @@ async function executeUserInput(params: ExecuteUserInputParams): Promise<void> {
commands.every(c => c.workload === firstWorkload)
? firstWorkload
: undefined
let autonomyRunIds: string[] | undefined
// Wrap the entire turn (processUserInput loop + onQuery) in an
// AsyncLocalStorage context. This is the ONLY way to correctly
@@ -469,131 +476,159 @@ async function executeUserInput(params: ExecuteUserInputParams): Promise<void> {
// context — isolated from the parent's continuation. A process-global
// mutable slot would be clobbered at the detached closure's first
// await by this function's synchronous return path. See state.ts.
await runWithWorkload(turnWorkload, async () => {
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i]!
const isFirst = i === 0
const result = await processUserInput({
input: cmd.value,
preExpansionInput: cmd.preExpansionValue,
mode: cmd.mode,
setToolJSX,
context: makeContext(),
pastedContents: isFirst ? cmd.pastedContents : undefined,
messages,
setUserInputOnProcessing: isFirst
? setUserInputOnProcessing
: undefined,
isAlreadyProcessing: !isFirst,
querySource,
canUseTool,
uuid: cmd.uuid,
ideSelection: isFirst ? ideSelection : undefined,
skipSlashCommands: cmd.skipSlashCommands,
bridgeOrigin: cmd.bridgeOrigin,
isMeta: cmd.isMeta,
skipAttachments: !isFirst,
})
// Stamp origin here rather than threading another arg through
// processUserInput → processUserInputBase → processTextPrompt → createUserMessage.
// Derive origin from mode for task-notifications — mirrors the origin
// derivation at messages.ts (case 'queued_command'); intentionally
// does NOT mirror its isMeta:true so idle-dequeued notifications stay
// visible in the transcript via UserAgentNotificationMessage.
const origin =
cmd.origin ??
(cmd.mode === 'task-notification'
? ({ kind: 'task-notification' } as const)
: undefined)
if (origin) {
for (const m of result.messages) {
if (m.type === 'user') m.origin = origin
try {
await runWithWorkload(turnWorkload, async () => {
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i]!
const isFirst = i === 0
if (cmd.autonomy?.runId) {
;(autonomyRunIds ??= []).push(cmd.autonomy.runId)
await markAutonomyRunRunning(cmd.autonomy.runId)
}
const result = await processUserInput({
input: cmd.value,
preExpansionInput: cmd.preExpansionValue,
mode: cmd.mode,
setToolJSX,
context: makeContext(),
pastedContents: isFirst ? cmd.pastedContents : undefined,
messages,
setUserInputOnProcessing: isFirst
? setUserInputOnProcessing
: undefined,
isAlreadyProcessing: !isFirst,
querySource,
canUseTool,
uuid: cmd.uuid,
ideSelection: isFirst ? ideSelection : undefined,
skipSlashCommands: cmd.skipSlashCommands,
bridgeOrigin: cmd.bridgeOrigin,
isMeta: cmd.isMeta,
skipAttachments: !isFirst,
})
// Stamp origin here rather than threading another arg through
// processUserInput → processUserInputBase → processTextPrompt → createUserMessage.
// Derive origin from mode for task-notifications — mirrors the origin
// derivation at messages.ts (case 'queued_command'); intentionally
// does NOT mirror its isMeta:true so idle-dequeued notifications stay
// visible in the transcript via UserAgentNotificationMessage.
const origin =
cmd.origin ??
(cmd.mode === 'task-notification'
? ({ kind: 'task-notification' } as const)
: undefined)
if (origin) {
for (const m of result.messages) {
if (m.type === 'user') m.origin = origin
}
}
newMessages.push(...result.messages)
if (isFirst) {
shouldQuery = result.shouldQuery
allowedTools = result.allowedTools
model = result.model
effort = result.effort
nextInput = result.nextInput
submitNextInput = result.submitNextInput
}
}
newMessages.push(...result.messages)
if (isFirst) {
shouldQuery = result.shouldQuery
allowedTools = result.allowedTools
model = result.model
effort = result.effort
nextInput = result.nextInput
submitNextInput = result.submitNextInput
}
}
queryCheckpoint('query_process_user_input_end')
if (fileHistoryEnabled()) {
queryCheckpoint('query_file_history_snapshot_start')
newMessages.filter(selectableUserMessagesFilter).forEach(message => {
void fileHistoryMakeSnapshot(
(updater: (prev: FileHistoryState) => FileHistoryState) => {
setAppState(prev => ({
...prev,
fileHistory: updater(prev.fileHistory),
}))
},
message.uuid,
queryCheckpoint('query_process_user_input_end')
if (fileHistoryEnabled()) {
queryCheckpoint('query_file_history_snapshot_start')
newMessages.filter(selectableUserMessagesFilter).forEach(message => {
void fileHistoryMakeSnapshot(
(updater: (prev: FileHistoryState) => FileHistoryState) => {
setAppState(prev => ({
...prev,
fileHistory: updater(prev.fileHistory),
}))
},
message.uuid,
)
})
queryCheckpoint('query_file_history_snapshot_end')
}
if (newMessages.length) {
// History is now added in the caller (onSubmit) for direct user submissions.
// This ensures queued command processing (notifications, already-queued user input)
// doesn't add to history, since those either shouldn't be in history or were
// already added when originally queued.
resetHistory()
setToolJSX({
jsx: null,
shouldHidePromptInput: false,
clearLocalJSX: true,
})
const primaryCmd = commands[0]
const primaryMode = primaryCmd?.mode ?? 'prompt'
const primaryInput =
primaryCmd && typeof primaryCmd.value === 'string'
? primaryCmd.value
: undefined
const shouldCallBeforeQuery = primaryMode === 'prompt'
await onQuery(
newMessages,
abortController,
shouldQuery,
allowedTools ?? [],
model
? resolveSkillModelOverride(model, mainLoopModel)
: mainLoopModel,
shouldCallBeforeQuery ? onBeforeQuery : undefined,
primaryInput,
effort,
)
})
queryCheckpoint('query_file_history_snapshot_end')
}
if (newMessages.length) {
// History is now added in the caller (onSubmit) for direct user submissions.
// This ensures queued command processing (notifications, already-queued user input)
// doesn't add to history, since those either shouldn't be in history or were
// already added when originally queued.
resetHistory()
setToolJSX({
jsx: null,
shouldHidePromptInput: false,
clearLocalJSX: true,
})
const primaryCmd = commands[0]
const primaryMode = primaryCmd?.mode ?? 'prompt'
const primaryInput =
primaryCmd && typeof primaryCmd.value === 'string'
? primaryCmd.value
: undefined
const shouldCallBeforeQuery = primaryMode === 'prompt'
await onQuery(
newMessages,
abortController,
shouldQuery,
allowedTools ?? [],
model
? resolveSkillModelOverride(model, mainLoopModel)
: mainLoopModel,
shouldCallBeforeQuery ? onBeforeQuery : undefined,
primaryInput,
effort,
)
} else {
// Local slash commands that skip messages (e.g., /model, /theme).
// Release the guard BEFORE clearing toolJSX to prevent spinner flash —
// the spinner formula checks: (!toolJSX || showSpinner) && isLoading.
// If we clear toolJSX while the guard is still reserved, spinner briefly
// shows. The finally below also calls cancelReservation (no-op if idle).
queryGuard.cancelReservation()
setToolJSX({
jsx: null,
shouldHidePromptInput: false,
clearLocalJSX: true,
})
resetHistory()
setAbortController(null)
}
// Handle nextInput from commands that want to chain (e.g., /discover activation)
if (nextInput) {
if (submitNextInput) {
enqueue({ value: nextInput, mode: 'prompt' })
} else {
params.onInputChange(nextInput)
// Local slash commands that skip messages (e.g., /model, /theme).
// Release the guard BEFORE clearing toolJSX to prevent spinner flash —
// the spinner formula checks: (!toolJSX || showSpinner) && isLoading.
// If we clear toolJSX while the guard is still reserved, spinner briefly
// shows. The finally below also calls cancelReservation (no-op if idle).
queryGuard.cancelReservation()
setToolJSX({
jsx: null,
shouldHidePromptInput: false,
clearLocalJSX: true,
})
resetHistory()
setAbortController(null)
}
// Handle nextInput from commands that want to chain (e.g., /discover activation)
if (nextInput) {
if (submitNextInput) {
enqueue({ value: nextInput, mode: 'prompt' })
} else {
params.onInputChange(nextInput)
}
}
}) // end runWithWorkload — ALS context naturally scoped, no finally needed
if (autonomyRunIds?.length) {
for (const runId of autonomyRunIds) {
const nextCommands = await finalizeAutonomyRunCompleted({
runId,
priority: 'later',
workload: turnWorkload,
})
for (const nextCommand of nextCommands) {
enqueue(nextCommand)
}
}
}
}) // end runWithWorkload — ALS context naturally scoped, no finally needed
} catch (error) {
if (autonomyRunIds?.length) {
for (const runId of autonomyRunIds) {
await finalizeAutonomyRunFailed({
runId,
error: String(error),
})
}
}
throw error
}
} finally {
// Safety net: release the guard reservation if processUserInput threw
// or onQuery was skipped. No-op if onQuery already ran (guard is idle