Skip to content

Commit 275e2ee

Browse files
CC11001100claude
andcommitted
perf(restore): optimize restore performance, add traceID logging, fix hotkey cooldown
- Replace all fixed usleep() in restore/toggle paths with poll-based verification - Add pollUntil() utility to SpaceController for condition-based waiting - Reduce hotkey cooldown from 800ms to 50ms (toggle itself provides natural gap) - Reduce dedup interval from 400ms to 150ms - Add full traceID propagation through HookEventHandler → ToggleEngine → SpaceController - Add duration markers (resolveDurationMs, terminalResolveMs, restoreMs, totalMs) - Fix misleading token regeneration warning in SettingsUI Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c9d6ea8 commit 275e2ee

11 files changed

Lines changed: 1700 additions & 653 deletions

Sources/HookEventHandler.swift

Lines changed: 69 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,14 @@ final class HookEventHandler {
9797
func handleUserPromptSubmit(
9898
payload: ClaudeHookPayload
9999
) -> (statusCode: Int, response: ClaudeHookResponse) {
100+
let traceID = makeOperationID(prefix: "ups")
101+
let handleStartedAt = Date()
100102
lastActivityBySession[payload.sessionID] = Date()
101103

102104
log(
103105
"[HookEventHandler] UserPromptSubmit triggered",
104106
fields: [
107+
"traceID": traceID,
105108
"sessionID": payload.sessionID,
106109
"autoRestoreEnabled": String(ClaudeHookPreferences.autoRestoreOnPromptSubmit),
107110
"cwd": payload.cwd ?? "nil"
@@ -133,6 +136,7 @@ final class HookEventHandler {
133136
"[HookEventHandler] UserPromptSubmit binding verification failed",
134137
level: .warn,
135138
fields: [
139+
"traceID": traceID,
136140
"sessionID": payload.sessionID,
137141
"pid": String(state.pid),
138142
"tty": state.tty ?? "nil"
@@ -156,23 +160,50 @@ final class HookEventHandler {
156160
title: state.title,
157161
capturedAt: state.createdAt
158162
)
163+
log(
164+
"[HookEventHandler] UserPromptSubmit binding resolved",
165+
level: .debug,
166+
fields: [
167+
"traceID": traceID,
168+
"sessionID": payload.sessionID,
169+
"windowID": String(state.windowID),
170+
"resolveDurationMs": String(elapsedMilliseconds(since: handleStartedAt))
171+
]
172+
)
159173
} else if let terminalCtx = payload.terminalCtx, terminalCtx.hasUsefulContext {
174+
let terminalResolveStart = Date()
160175
identity = WindowManager.shared.findWindowByTerminalContext(terminalCtx)
176+
let terminalResolveMs = elapsedMilliseconds(since: terminalResolveStart)
161177
if let identity {
162178
log(
163-
"[HookEventHandler] UserPromptSubmit no binding, resolved via terminal context",
179+
"[HookEventHandler] UserPromptSubmit resolved via terminal context",
164180
fields: [
181+
"traceID": traceID,
165182
"sessionID": payload.sessionID,
166183
"resolvedWindowID": String(identity.windowID),
167-
"app": identity.appName ?? "unknown"
184+
"app": identity.appName ?? "unknown",
185+
"terminalResolveMs": String(terminalResolveMs)
186+
]
187+
)
188+
} else {
189+
log(
190+
"[HookEventHandler] UserPromptSubmit terminal context resolve returned nil",
191+
level: .warn,
192+
fields: [
193+
"traceID": traceID,
194+
"sessionID": payload.sessionID,
195+
"terminalResolveMs": String(terminalResolveMs)
168196
]
169197
)
170198
}
171199
} else {
172200
log(
173201
"[HookEventHandler] UserPromptSubmit no binding and no terminal context",
174202
level: .warn,
175-
fields: ["sessionID": payload.sessionID]
203+
fields: [
204+
"traceID": traceID,
205+
"sessionID": payload.sessionID
206+
]
176207
)
177208
return (
178209
200,
@@ -202,6 +233,7 @@ final class HookEventHandler {
202233
log(
203234
"[HookEventHandler] UserPromptSubmit window not on main screen",
204235
fields: [
236+
"traceID": traceID,
205237
"sessionID": payload.sessionID,
206238
"windowID": String(identity.windowID)
207239
]
@@ -224,9 +256,31 @@ final class HookEventHandler {
224256
}
225257

226258
if record.isValid(mainScreenFrame: mainScreen.frame) {
259+
let restoreStart = Date()
260+
log(
261+
"[HookEventHandler] UserPromptSubmit calling ToggleEngine.restore",
262+
level: .info,
263+
fields: [
264+
"traceID": traceID,
265+
"windowID": String(identity.windowID),
266+
"preRestoreMs": String(elapsedMilliseconds(since: handleStartedAt))
267+
]
268+
)
227269
let success = engine.restore(
228270
windowID: identity.windowID,
229-
triggerSource: "user_prompt_submit"
271+
triggerSource: "user_prompt_submit",
272+
traceID: traceID
273+
)
274+
let restoreMs = elapsedMilliseconds(since: restoreStart)
275+
log(
276+
"[HookEventHandler] UserPromptSubmit restore completed",
277+
level: success ? .info : .warn,
278+
fields: [
279+
"traceID": traceID,
280+
"success": String(success),
281+
"restoreMs": String(restoreMs),
282+
"totalMs": String(elapsedMilliseconds(since: handleStartedAt))
283+
]
230284
)
231285
if success {
232286
return (
@@ -252,16 +306,25 @@ final class HookEventHandler {
252306
)
253307
}
254308
} else {
255-
// corrupted state(两个 frame 都在主屏),清除
256309
engine.clear(windowID: identity.windowID)
310+
log(
311+
"[HookEventHandler] UserPromptSubmit toggle record corrupted, cleared",
312+
level: .warn,
313+
fields: [
314+
"traceID": traceID,
315+
"windowID": String(identity.windowID)
316+
]
317+
)
257318
}
258319
}
259320

260321
log(
261322
"[HookEventHandler] UserPromptSubmit no toggle state found",
262323
fields: [
324+
"traceID": traceID,
263325
"sessionID": payload.sessionID,
264-
"windowID": String(identity.windowID)
326+
"windowID": String(identity.windowID),
327+
"totalMs": String(elapsedMilliseconds(since: handleStartedAt))
265328
]
266329
)
267330
return (
@@ -274,98 +337,6 @@ final class HookEventHandler {
274337
)
275338
}
276339

277-
// MARK: - Perform Restore
278-
279-
private func performRestoreFromState(
280-
payload: ClaudeHookPayload,
281-
toggleState: WindowState
282-
) -> (statusCode: Int, response: ClaudeHookResponse) {
283-
let wm = WindowManager.shared
284-
285-
guard let origFrame = toggleState.originalFrame,
286-
let tgtFrame = toggleState.targetFrame else {
287-
return (200, ClaudeHookResponse(ok: true, code: "no_frame_data", message: "No frame data", sessionID: payload.sessionID, handled: false))
288-
}
289-
290-
let savedState = WindowManager.SavedWindowState(
291-
id: "\(toggleState.pid)_\(toggleState.tty ?? "none")",
292-
pid: toggleState.pid,
293-
bundleIdentifier: toggleState.bundleIdentifier,
294-
appName: toggleState.appName,
295-
windowID: toggleState.windowID,
296-
windowNumber: toggleState.axWindowNumber,
297-
title: toggleState.title,
298-
originalFrame: WindowManager.RectPayload(origFrame),
299-
targetFrame: WindowManager.RectPayload(tgtFrame),
300-
sourceSpaceIndex: toggleState.sourceSpace,
301-
targetSpaceIndex: nil,
302-
sourceYabaiDisplayIndex: toggleState.sourceYabaiDisp,
303-
sourceDisplaySpaceIndex: toggleState.sourceDispSpace,
304-
sourceDisplayIndex: toggleState.sourceDisplay,
305-
sourceDisplayID: nil,
306-
targetDisplayIndex: toggleState.targetDisplay,
307-
restoreReason: toggleState.toggleReason,
308-
sessionID: toggleState.sessionID,
309-
savedAt: toggleState.toggledAt ?? Date()
310-
)
311-
312-
wm.hydrateMemory(from: savedState, window: nil)
313-
314-
// 验证找到的窗口确实在 targetFrame 附近
315-
if let resolvedWindow = wm.lastWindowElement,
316-
let resolvedFrame = wm.frame(of: resolvedWindow) {
317-
if !toggleState.isNearTarget(currentFrame: resolvedFrame) {
318-
log(
319-
"[HookEventHandler] UserPromptSubmit restore aborted: window moved from target pos resolvedX=\(resolvedFrame.origin.x) resolvedY=\(resolvedFrame.origin.y)",
320-
level: .warn,
321-
fields: ["sessionID": payload.sessionID]
322-
)
323-
SessionWindowRegistry.shared.clearToggleState(windowID: toggleState.windowID)
324-
return (
325-
200,
326-
ClaudeHookResponse(
327-
ok: true, code: "window_moved_skip",
328-
message: "Window position changed, skipping stale restore",
329-
sessionID: payload.sessionID, handled: false
330-
)
331-
)
332-
}
333-
}
334-
335-
log(
336-
"[HookEventHandler] UserPromptSubmit restoring window",
337-
fields: [
338-
"sessionID": payload.sessionID,
339-
"pid": String(toggleState.pid),
340-
"tty": toggleState.tty ?? "nil",
341-
"app": toggleState.appName ?? "unknown",
342-
"windowID": String(toggleState.windowID),
343-
"originalFrame": String(describing: origFrame),
344-
"targetFrame": String(describing: tgtFrame)
345-
]
346-
)
347-
348-
wm.restore(
349-
operationID: makeOperationID(prefix: "hook-restore"),
350-
triggerSource: "user_prompt_submit"
351-
)
352-
353-
SessionWindowRegistry.shared.reactivate(sessionID: payload.sessionID)
354-
SessionWindowRegistry.shared.clearToggleState(windowID: toggleState.windowID)
355-
356-
SessionWindowRegistry.shared.setLastEventDescription(
357-
"UserPromptSubmit 恢复窗口:\(toggleState.appName ?? "Unknown")"
358-
)
359-
return (
360-
200,
361-
ClaudeHookResponse(
362-
ok: true, code: "window_restored",
363-
message: "Window restored to original position",
364-
sessionID: payload.sessionID, handled: true
365-
)
366-
)
367-
}
368-
369340
// MARK: - Stop
370341

371342
func handleStop(

Sources/HotKeyManager.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ final class HotKeyManager: ObservableObject {
3131
private var isToggleInFlight = false
3232
private var lastToggleTriggeredAt: Date = .distantPast
3333
private var lastToggleCompletedAt: Date = .distantPast
34-
private let toggleDedupInterval: TimeInterval = 0.40
35-
private let toggleCooldownInterval: TimeInterval = 0.80
34+
private let toggleDedupInterval: TimeInterval = 0.15
35+
private let toggleCooldownInterval: TimeInterval = 0.05
3636

3737
private init() {
3838
currentHotKey = Self.loadStoredHotKey()

Sources/SettingsUI.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1215,7 +1215,7 @@ private struct SettingsView: View {
12151215
}
12161216

12171217
if !hookToken.isEmpty {
1218-
Text("重新生成后需重新安装 Hook,否则 Claude Code 会使用旧 Token 导致鉴权失败")
1218+
Text("Token 已自动同步到 Hook 脚本配置,无需重新安装")
12191219
.font(.system(size: 12))
12201220
.foregroundStyle(.secondary)
12211221
.fixedSize(horizontal: false, vertical: true)

0 commit comments

Comments
 (0)