mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
Squashed merge of: 1. fix/mcp-tsc-errors — 修复上游 MCP 重构后的 tsc 错误和测试失败 2. feat/pipe-mute-disconnect — Pipe IPC 逻辑断开、/lang 命令、mute 状态机 3. feat/stub-recovery-all — 实现全部 stub 恢复 (task 001-012) 4. feat/kairos-activation — KAIROS 激活解除阻塞 + 工具实现 5. codex/openclaw-autonomy-pr — 自治权限系统、运行记录、managed flows Additional: 6. daemon/job 命令层级化重构 (subcommand 架构) 7. 跨平台后台引擎抽象 (detached/tmux engines) 8. 修复 src/ 中 43 个预存在的 TypeScript 类型错误 9. 修复 langfuse isolated test mock 完整性 10. 修复 CodeRabbit 审查的 Critical/Major/Minor 问题 11. remote-control-server logger 抽象 (测试 stderr 静默化) 12. /simplify 审查修复 (代码复用、质量、效率)
142 lines
4.4 KiB
TypeScript
142 lines
4.4 KiB
TypeScript
/**
|
|
* usePipeMuteSync — Sync master's UI selection state to slave relay mute flags.
|
|
*
|
|
* Watches routeMode, selectedPipes, slave client registry, and send-override
|
|
* changes. When a slave is deselected or routeMode switches to 'local', sends
|
|
* relay_mute. When re-selected, sends relay_unmute. Also maintains the
|
|
* master-side muted set for in-flight message filtering.
|
|
*
|
|
* Feature-gated by UDS_INBOX (conditional import in REPL.tsx).
|
|
*/
|
|
import { useEffect, useRef, useSyncExternalStore } from 'react'
|
|
import { useAppState } from '../state/AppState.js'
|
|
import { getPipeIpc } from '../utils/pipeTransport.js'
|
|
import {
|
|
setMasterMutedPipes,
|
|
clearMasterMutedPipes,
|
|
hasSendOverride,
|
|
clearSendOverrides,
|
|
subscribeSendOverride,
|
|
getSendOverrideVersion,
|
|
} from '../utils/pipeMuteState.js'
|
|
import {
|
|
getAllSlaveClients,
|
|
subscribeToSlaveClientRegistry,
|
|
getSlaveClientRegistryVersion,
|
|
} from './useMasterMonitor.js'
|
|
|
|
type UsePipeMuteSyncDeps = {
|
|
setToolUseConfirmQueue: (action: React.SetStateAction<Record<string, unknown>[]>) => void
|
|
}
|
|
|
|
export function usePipeMuteSync({
|
|
setToolUseConfirmQueue,
|
|
}: UsePipeMuteSyncDeps): void {
|
|
// Subscribe to individual scalars to avoid object-selector re-render churn
|
|
// (AppState.tsx warns against object-returning selectors)
|
|
const routeMode = useAppState(
|
|
s => (getPipeIpc(s).routeMode as 'selected' | 'local') ?? 'selected',
|
|
)
|
|
const selectedPipes: string[] = useAppState(
|
|
s => (getPipeIpc(s).selectedPipes as string[]) ?? [],
|
|
)
|
|
|
|
// Subscribe to slave client registry changes
|
|
const registryVersion = useSyncExternalStore(
|
|
subscribeToSlaveClientRegistry,
|
|
getSlaveClientRegistryVersion,
|
|
getSlaveClientRegistryVersion,
|
|
)
|
|
|
|
// Subscribe to send-override changes so mute recalculates after /send completes
|
|
const sendOverrideVersion = useSyncExternalStore(
|
|
subscribeSendOverride,
|
|
getSendOverrideVersion,
|
|
getSendOverrideVersion,
|
|
)
|
|
|
|
const prevMutedRef = useRef<Set<string>>(new Set())
|
|
|
|
useEffect(() => {
|
|
const slaves = getAllSlaveClients()
|
|
|
|
// Compute which slaves should be muted now
|
|
const nextMuted = new Set<string>()
|
|
if (routeMode === 'local') {
|
|
// All connected slaves muted
|
|
for (const name of slaves.keys()) {
|
|
if (!hasSendOverride(name)) {
|
|
nextMuted.add(name)
|
|
}
|
|
}
|
|
} else {
|
|
// routeMode === 'selected': mute slaves NOT in selectedPipes
|
|
const selectedSet = new Set(selectedPipes)
|
|
for (const name of slaves.keys()) {
|
|
if (!selectedSet.has(name) && !hasSendOverride(name)) {
|
|
nextMuted.add(name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 1: Update master-side muted set FIRST (before sending control packets)
|
|
setMasterMutedPipes(nextMuted)
|
|
|
|
const prevMuted = prevMutedRef.current
|
|
|
|
// Step 2: For newly muted slaves — abort pending permissions, then send relay_mute
|
|
for (const name of nextMuted) {
|
|
if (!prevMuted.has(name)) {
|
|
// Abort pending permission prompts for this slave
|
|
setToolUseConfirmQueue((queue: Record<string, unknown>[]) => {
|
|
const toAbort = queue.filter(
|
|
(item: Record<string, unknown>) => item.pipeName === name,
|
|
)
|
|
for (const item of toAbort) {
|
|
try {
|
|
;(item.onAbort as (() => void) | undefined)?.()
|
|
} catch {
|
|
// onAbort may throw if client disconnected — safe to ignore
|
|
}
|
|
}
|
|
return queue.filter((item: Record<string, unknown>) => item.pipeName !== name)
|
|
})
|
|
|
|
// Send relay_mute to slave
|
|
const client = slaves.get(name)
|
|
if (client?.connected) {
|
|
try {
|
|
client.send({ type: 'relay_mute' })
|
|
} catch {
|
|
// send may fail if socket is closing — non-fatal
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 3: For newly unmuted slaves — send relay_unmute
|
|
for (const name of prevMuted) {
|
|
if (!nextMuted.has(name)) {
|
|
const client = slaves.get(name)
|
|
if (client?.connected) {
|
|
try {
|
|
client.send({ type: 'relay_unmute' })
|
|
} catch {
|
|
// non-fatal
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
prevMutedRef.current = nextMuted
|
|
}, [routeMode, selectedPipes, registryVersion, sendOverrideVersion, setToolUseConfirmQueue])
|
|
|
|
// Cleanup on unmount: clear all master-side mute state
|
|
useEffect(() => {
|
|
return () => {
|
|
clearMasterMutedPipes()
|
|
clearSendOverrides()
|
|
}
|
|
}, [])
|
|
}
|