From 7b6cb98e7ae6b22e105007ca5d7ccc0b7160211b Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 18 Sep 2026 12:03:02 +0700 Subject: [PATCH] fix: decide the content mode commands, name the payload digest, and wait out the stream producer --- TablePro/Core/MCP/MCPAuditLogger.swift | 9 ++++++--- ...inSplitViewController+MenuValidation.swift | 19 ++++++------------- .../Database/PluginStreamAbortTests.swift | 7 +++++-- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/TablePro/Core/MCP/MCPAuditLogger.swift b/TablePro/Core/MCP/MCPAuditLogger.swift index bb4169e799..4abe203572 100644 --- a/TablePro/Core/MCP/MCPAuditLogger.swift +++ b/TablePro/Core/MCP/MCPAuditLogger.swift @@ -266,14 +266,17 @@ enum MCPAuditLogger { payload: Data ) { let digest = SHA256.hash(data: payload).compactMap { String(format: "%02x", $0) }.joined() - let fingerprint = String(digest.prefix(16)) + /// Named for what it digests. In this surface a fingerprint means a pairing token's, which + /// no log line may carry, and `MCPAuditLogStorageTests` reads that vocabulary rather than + /// the value. + let payloadDigest = String(digest.prefix(16)) serverTool.info( """ Outbound tool: server=\(serverName, privacy: .public) \ tool=\(toolName, privacy: .public) \ connection=\(connectionId?.uuidString ?? "-", privacy: .public) \ bytes=\(payload.count, privacy: .public) \ - payload=\(fingerprint, privacy: .public) + payload=\(payloadDigest, privacy: .public) """ ) @@ -287,7 +290,7 @@ enum MCPAuditLogger { "serverName=\(truncate(serverName, to: messageExcerptLimit))", "session=\(sessionId.uuidString)", "bytes=\(payload.count)", - "payload=\(fingerprint)" + "payload=\(payloadDigest)" ].joined(separator: " ") ) } diff --git a/TablePro/Core/Services/Infrastructure/MainSplitViewController+MenuValidation.swift b/TablePro/Core/Services/Infrastructure/MainSplitViewController+MenuValidation.swift index 5ee15c88e4..5b72e2f0cf 100644 --- a/TablePro/Core/Services/Infrastructure/MainSplitViewController+MenuValidation.swift +++ b/TablePro/Core/Services/Infrastructure/MainSplitViewController+MenuValidation.swift @@ -205,6 +205,12 @@ extension MainSplitViewController: NSMenuItemValidation { case #selector(explainQueryWithAI(_:)), #selector(optimizeQueryWithAI(_:)): return context.isConnected && context.hasQueryText && AppSettingsManager.shared.ai.enabled + /// Reachable while the connection is still dialling: agent mode draws the prompt the user + /// typed, which is exactly what they are waiting with, so gating on `isConnected` would make + /// the command dead in the one state it is most wanted. + case #selector(setContentModeFromMenu(_:)), + #selector(toggleContentModeFromMenu(_:)): + return context.hasSelectedWorkspace && AppSettingsManager.shared.ai.enabled case #selector(toggleFold(_:)), #selector(foldAll(_:)), #selector(unfoldAll(_:)): return context.hasEditorForFind case #selector(removeInvisibleCharacters(_:)): @@ -479,25 +485,12 @@ extension MainSplitViewController: NSMenuItemValidation { /// rather than staying enabled over a pane that would refuse to open. if action == #selector(toggleAssistant(_:)) { return canRevealAssistant } if action == #selector(setResultView(_:)) { return canShowResultView(menuItem) } - /// Reachable while the connection is still dialling: agent mode draws the prompt the user - /// typed, which is exactly what they are waiting with, so gating on `connected` would make - /// the command dead in the one state it is most wanted. - if action == #selector(setContentModeFromMenu(_:)) || action == #selector(toggleContentModeFromMenu(_:)) { - return canSwitchContentMode - } if action == #selector(setSafeModeLevel(_:)) { return canChooseSafeModeLevel(menuItem) } if action == #selector(requestDisconnect) { return canDisconnect } if action == #selector(retryConnection) { return canReconnect } return Self.isEnabled(action, context: menuValidationContext) } - /// The mode is per connection, so the command needs one selected and the AI feature on. It does - /// not need a live session: the surface it switches to is what the user types into while one is - /// being made. - private var canSwitchContentMode: Bool { - workspaces.selected != nil && AppSettingsManager.shared.ai.enabled - } - private func isCurrentContentMode(_ menuItem: NSMenuItem) -> Bool { guard let raw = menuItem.representedObject as? String, let mode = ConnectionWorkspaceContentMode(rawValue: raw) else { return false } diff --git a/TableProTests/Core/Database/PluginStreamAbortTests.swift b/TableProTests/Core/Database/PluginStreamAbortTests.swift index 0c5a627bbf..0f51c5f3c3 100644 --- a/TableProTests/Core/Database/PluginStreamAbortTests.swift +++ b/TableProTests/Core/Database/PluginStreamAbortTests.swift @@ -10,7 +10,6 @@ import Testing @Suite("Row stream abort reaches a producer that polls it") struct PluginStreamAbortTests { - @Test("Terminating the stream sets the flag, and a serial-queue producer stops early") func serialQueueProducerStopsEarly() async throws { let queue = DispatchQueue(label: "test.stream.abort.serial") @@ -111,7 +110,11 @@ struct PluginStreamAbortTests { if seen >= 2 { break } } - try await Task.sleep(for: .seconds(0.6)) + /// Waited for rather than slept through. The producer paces itself at 20ms a row, so a + /// fixed wait asserts the runner's load as much as the stream's behaviour: CI saw 19 of 20. + for _ in 0 ..< 100 where counter.value < 20 { + try await Task.sleep(for: .milliseconds(50)) + } #expect(counter.value == 20) _ = stream }