Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions TablePro/Core/MCP/MCPAuditLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"""
)

Expand All @@ -287,7 +290,7 @@ enum MCPAuditLogger {
"serverName=\(truncate(serverName, to: messageExcerptLimit))",
"session=\(sessionId.uuidString)",
"bytes=\(payload.count)",
"payload=\(fingerprint)"
"payload=\(payloadDigest)"
].joined(separator: " ")
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(_:)):
Expand Down Expand Up @@ -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 }
Expand Down
7 changes: 5 additions & 2 deletions TableProTests/Core/Database/PluginStreamAbortTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
}
Expand Down
Loading