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

Large diffs are not rendered by default.

131 changes: 102 additions & 29 deletions .agents/skills/develop-lithe/references/logging.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ final class CommitFileDeletionFeatureModel: ObservableObject {
for url in urls {
guard isCurrent(), !Task.isCancelled, contains(url, in: root), files.fileExists(at: url) else { return false }
guard !hasUnsavedEdits(url) else { reportUnsaved(); return false }
try await Task.detached(priority: .userInitiated) { try files.trashItem(at: url) }.value
try await UserActionContext.detached(priority: .userInitiated) { try files.trashItem(at: url) }.value
if isCurrent(), !hasUnsavedEdits(url) { closeCleanDocuments(url) }
}
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ final class DocumentFeatureModel: ObservableObject {
result = .failure(StandaloneFileOpenFailure(error: error))
}
} else {
result = await Task.detached(priority: .userInitiated) {
result = await UserActionContext.detached(priority: .userInitiated) {
Self.readStandaloneFile(at: normalizedURL, using: fileStorage, logSink: self.logSink)
.map { DocumentReadDetails(text: $0, encoding: .utf8) }
}.value
Expand Down Expand Up @@ -605,7 +605,7 @@ final class DocumentFeatureModel: ObservableObject {
var openFailure: StandaloneFileOpenFailure?
if relativePath == nil, !fileOperations.supportsDocumentEncoding {
let fileStorage = self.fileStorage
let result = await Task.detached(priority: .userInitiated) {
let result = await UserActionContext.detached(priority: .userInitiated) {
Self.readStandaloneFile(at: normalizedURL, using: fileStorage, logSink: self.logSink)
}.value
switch result {
Expand Down Expand Up @@ -645,7 +645,7 @@ final class DocumentFeatureModel: ObservableObject {
// suffix alone must not hide an actual encoding error.
let fileStorage = self.fileStorage
let logSink = self.logSink
let header = await Task.detached(priority: .userInitiated) { () -> Data? in
let header = await UserActionContext.detached(priority: .userInitiated) { () -> Data? in
do {
return try fileStorage.readPrefix(from: normalizedURL, byteCount: BinaryFileViewerRegistry.headerByteCount)
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,20 @@ final class PluginNativeUIFeature: ObservableObject, PluginWebPageHandle {
private let request: (String, ToolingJSONValue) async throws -> ToolingJSONValue
private let endRequests: () -> Void
private let saveState: (State) -> Void
private let pluginID: String
private let logSink: (any LogSink)?
private var task: Task<Void, Never>?
private var generation = UUID()
private var isClosed = false

init(declaration: PluginNativeUI, state: State = .init(),
validateNodes: @escaping (PluginNativeUI, ToolingJSONValue) throws -> [PluginNativeUI.Node],
request: @escaping (String, ToolingJSONValue) async throws -> ToolingJSONValue,
close: @escaping () -> Void = {}, saveState: @escaping (State) -> Void = { _ in }) {
close: @escaping () -> Void = {}, saveState: @escaping (State) -> Void = { _ in },
pluginID: String = "", logSink: (any LogSink)? = nil) {
self.declaration = declaration
self.pluginID = pluginID
self.logSink = logSink
self.nodes = declaration.nodes
var restored = state
restored.values = state.values?.filter { id, value in
Expand Down Expand Up @@ -139,9 +144,14 @@ final class PluginNativeUIFeature: ObservableObject, PluginWebPageHandle {
@discardableResult
func perform(_ action: PluginNativeUI.Action) -> Task<Void, Never>? {
guard canPerform(action) else { return nil }
return start(action.kind == .refresh ? nil : action)
return UserActionTrace.run(actionID(action), surface: .plugin, sink: logSink) {
start(action.kind == .refresh ? nil : action)
}
}

/// The action log names a plugin action by its declared ids, never its title.
func actionID(_ action: PluginNativeUI.Action) -> String { "plugin:\(pluginID)/\(action.id)" }

/// Hiding/disabling a window cancels every request; late replies cannot publish.
func close() {
isClosed = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ final class VcsProvidersFeatureModel: ObservableObject {
let planner = planner
let generation = generation
detectionTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
let result = await UserActionContext.detached(priority: .userInitiated) {
Result {
try planner.detectRoots(
workspaceURL: workspaceURL, manifests: sources.map(\.manifest), mappings: mappings
Expand Down Expand Up @@ -685,7 +685,7 @@ final class VcsProvidersFeatureModel: ObservableObject {
guard !sources.isEmpty else { return true }
let mappings = mappingStore.mappings(workspaceURL: workspaceURL)
let planner = planner, logSink = logSink, generation = generation
let detection = await Task.detached(priority: .userInitiated) { () -> VcsDetection? in
let detection = await UserActionContext.detached(priority: .userInitiated) { () -> VcsDetection? in
do { return try planner.detectRoots(workspaceURL: workspaceURL, manifests: sources.map(\.manifest), mappings: mappings) }
catch {
logSink?.emit(VcsRootsFallbackSelected(error: error, stage: "detection"))
Expand Down Expand Up @@ -741,7 +741,7 @@ final class VcsProvidersFeatureModel: ObservableObject {
guard paths.count == changes.count else { return false }
let planner = planner, roots = allRootIDs, ambiguous = ambiguous.map(\.id), generation = generation
do {
let owners = try await Task.detached(priority: .userInitiated) {
let owners = try await UserActionContext.detached(priority: .userInitiated) {
try planner.owners(roots: roots, ambiguous: ambiguous, paths: paths)
}.value
guard self.generation == generation, !Task.isCancelled,
Expand Down Expand Up @@ -846,7 +846,7 @@ final class VcsProvidersFeatureModel: ObservableObject {
let rootIDs = allRootIDs
let ambiguousIDs = ambiguous.map(\.id)
let generation = generation
let owners = await Task.detached(priority: .userInitiated) { () -> [String?]? in
let owners = await UserActionContext.detached(priority: .userInitiated) { () -> [String?]? in
do { return try planner.owners(roots: rootIDs, ambiguous: ambiguousIDs, paths: [sourcePath, destinationPath]) }
catch {
logSink?.emit(VcsRootsFallbackSelected(error: error, stage: "ownership"))
Expand Down Expand Up @@ -926,7 +926,7 @@ final class VcsProvidersFeatureModel: ObservableObject {
roots[index].negotiation = reply.negotiation
}
let planner = planner
let value = try await Task.detached(priority: .userInitiated) {
let value = try await UserActionContext.detached(priority: .userInitiated) {
try planner.decodeReply(
operation, capabilities: reply.negotiation.capabilities, reply: reply.value, as: Value.self
)
Expand Down
6 changes: 6 additions & 0 deletions macos/Sources/Lithe/Core/Rust/RustCoreBridge+Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ extension RustCoreBridge {
func drainCoreLogEvents() throws -> CoreLogDrain {
try executeResult(command: "diagnostics.drainCoreEvents", payload: [String: String]()).get()
}
private struct CoreEventConfiguration: Encodable { let debug: Bool; let roots: [String]? }
/// Core skips building DEBUG facts the host would drop; `roots: nil` admits every root.
func configureCoreEvents(debug: Bool, roots: [String]?) throws {
let _: [String: String] = try executeResult(command: "diagnostics.configureCoreEvents",
payload: CoreEventConfiguration(debug: debug, roots: roots)).get()
}

/// Core calls `callback` at most once per drain cycle, from any thread, while
/// it owns the installation. The callback must only schedule host work.
Expand Down
26 changes: 25 additions & 1 deletion macos/Sources/Lithe/Core/Rust/RustCoreBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore {
let payload: Payload
var gitExecution: GitExecutionOptions? = nil
var remoteWorkspace: RemoteWorkspace? = nil
var origin: RequestOrigin? = nil
}

/// Who started a request. Core logs user requests at INFO under the action's
/// operation and keeps background or unattributed requests at DEBUG.
struct RequestOrigin: Encodable, Equatable {
var source: String?
var trigger: String?
var operation: String?

/// Long-lived descendants of an old action keep its correlation but no
/// longer claim to be user work; a background Git poller always wins.
static func resolve(action: UserAction?, gitSource: GitExecutionSource,
now: ContinuousClock.Instant = .now) -> RequestOrigin? {
if gitSource == .background { return RequestOrigin(source: GitExecutionSource.background.rawValue) }
if let action {
let fresh = action.startedAt.duration(to: now) < .seconds(60)
return RequestOrigin(source: fresh ? GitExecutionSource.user.rawValue : nil,
trigger: action.id, operation: action.operationID)
}
return gitSource == .unknown ? nil : RequestOrigin(source: gitSource.rawValue)
}
}

private struct Envelope<Data: Decodable>: Decodable {
Expand Down Expand Up @@ -4629,6 +4651,7 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore {
var gitOptions = gitPreferences?.snapshot ?? GitExecutionOptions()
gitOptions.interactive = execution != nil
gitOptions.source = execution?.source ?? GitExecutionSource.current
let origin = RequestOrigin.resolve(action: execution?.action ?? UserActionContext.current, gitSource: gitOptions.source)
let remoteWorkspace: RemoteWorkspace?
switch Result(catching: { try self.remoteWorkspace(for: command, payload: payload) }) {
case .success(let value): remoteWorkspace = value
Expand All @@ -4643,7 +4666,8 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore {
command: command,
payload: payload,
gitExecution: (observesGit || command == "workspace.searchEverywhere") ? gitOptions : nil,
remoteWorkspace: remoteWorkspace
remoteWorkspace: remoteWorkspace,
origin: origin
)
),
let request = String(data: requestData, encoding: .utf8),
Expand Down
Loading
Loading