Improved progress display

Previously, the progress bar updated only after completing an entire table.

For large tables such as LIBARC, this could make the application appear to have stopped responding.

Phase 10 updates the interface while rows are being exported.
This commit is contained in:
Mark Gregory
2026-10-16 22:20:48 +01:00
parent d479d3c07b
commit fb1430ad1a
21 changed files with 439 additions and 111 deletions
+11
View File
@@ -0,0 +1,11 @@
# Phase 14 — MySQL Account Management
Adds a **Manage Accounts…** sheet under Administration → Users and Privileges.
Operations: create account, change password, lock/unlock account, grant/revoke an individual database privilege. Every operation displays a SQL preview and requires an explicit confirmation. Passwords are masked in previews, never persisted by the account management form, and cleared after execution. Privileges are chosen from an allowlist. User, host, and database inputs use conservative character allowlists.
The existing account list and Show Grants remain available. Auto-refresh is switched off when account management opens, and the account list refreshes on dismissal.
**Limitations:** Requires sufficient MySQL privileges. Only basic ASCII account/host/database names supported. No account deletion, global privileges, roles, or complex grant options. SQL mode NO_BACKSLASH_ESCAPES is rejected for password operations. Password values are sent using the native query protocol, which may expose them in server-side SQL logs; parameterized MySQL account DDL isn't implemented. No automatic rollback for account changes.
**Validation:** Static/source inspection and ZIP integrity only. This environment cannot compile the macOS Xcode target or verify changes against a MySQL server. Test on a disposable MySQL instance before production use.
+197 -1
View File
@@ -1370,6 +1370,7 @@ struct WorkbenchAdministrationView: View {
@State private var grants: [String] = []
@State private var grantsError: String? = nil
@State private var showingGrants = false
@State private var showingAccountManager = false
private var filteredRows: [[String]] {
guard !searchText.isEmpty else { return result.rows }
@@ -1434,8 +1435,13 @@ struct WorkbenchAdministrationView: View {
Task { await showGrants() }
}
.disabled(selectedAccount == nil)
Button("Manage Accounts…") {
autoRefresh = false
showingAccountManager = true
}
.disabled(profile == nil)
Spacer()
Text("Read-only account inspection")
Text("Changes require confirmation")
.foregroundStyle(.secondary)
}
.padding(.horizontal, 16)
@@ -1457,6 +1463,16 @@ struct WorkbenchAdministrationView: View {
await refresh()
}
}
.sheet(isPresented: $showingAccountManager, onDismiss: {
Task { await refresh() }
}) {
if let profile {
WorkbenchAccountManagerView(
profile: profile,
selectedAccount: selectedAccount
)
}
}
.sheet(isPresented: $showingGrants) {
VStack(alignment: .leading, spacing: 12) {
Text("Grants: \(selectedAccount ?? "")")
@@ -2897,3 +2913,183 @@ struct WorkbenchBackupManifest: Codable {
return manifest
}
}
// MARK: - Phase 14: Explicitly confirmed MySQL account administration
private enum WorkbenchAccountAction: String, CaseIterable, Identifiable {
case create = "Create Account"
case password = "Change Password"
case lock = "Lock Account"
case unlock = "Unlock Account"
case grant = "Grant Privileges"
case revoke = "Revoke Privileges"
var id: String { rawValue }
}
private struct WorkbenchAccountManagerView: View {
let profile: ConnectionProfile
let selectedAccount: String?
@Environment(\.dismiss) private var dismiss
@State private var action: WorkbenchAccountAction = .create
@State private var username = ""
@State private var hostname = "%"
@State private var password = ""
@State private var databaseName = ""
@State private var privilege = "SELECT"
@State private var previewSQL = ""
@State private var confirm = false
@State private var running = false
@State private var status: String?
@State private var failure: String?
private let privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "INDEX", "REFERENCES", "EXECUTE", "CREATE VIEW", "SHOW VIEW", "TRIGGER"]
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Manage MySQL Accounts").font(.title2.bold())
Text("Account changes run on \(profile.name). Review the SQL before applying it.")
.foregroundStyle(.secondary)
Form {
Picker("Operation", selection: $action) {
ForEach(WorkbenchAccountAction.allCases) { option in
Text(option.rawValue).tag(option)
}
}
TextField("Username", text: $username)
.textContentType(.username)
TextField("Host", text: $hostname)
if action == .create || action == .password {
SecureField("Password", text: $password)
}
if action == .grant || action == .revoke {
TextField("Database", text: $databaseName)
Picker("Privilege", selection: $privilege) {
ForEach(privileges, id: \.self) { item in
Text(item).tag(item)
}
}
}
}
.formStyle(.grouped)
if let failure {
Text(failure).foregroundStyle(.red).textSelection(.enabled)
}
if let status {
Text(status).foregroundStyle(.green)
}
HStack {
Button("Close") { dismiss() }
Spacer()
Button("Review SQL…") { review() }
.disabled(running)
.keyboardShortcut(.defaultAction)
}
}
.padding(20)
.frame(width: 590)
.onAppear {
if let selectedAccount {
let parts = selectedAccount.split(separator: "\u{001f}", omittingEmptySubsequences: false)
if parts.count == 2 {
username = String(parts[0])
hostname = String(parts[1])
}
}
}
.alert("Confirm \(action.rawValue)", isPresented: $confirm) {
Button("Cancel", role: .cancel) { previewSQL = "" }
Button("Apply Change", role: .destructive) {
Task { await execute() }
}
} message: {
// Passwords are never shown in the SQL preview.
Text("Run this operation against \(profile.name)?\n\n\(previewSQL)")
}
}
// Strict account and database validation keeps identifiers out of SQL syntax.
private func sql() throws -> (command: String, preview: String) {
let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-%")
guard !username.isEmpty, username.unicodeScalars.allSatisfy({ allowed.contains($0) }),
!hostname.isEmpty, hostname.unicodeScalars.allSatisfy({ allowed.contains($0) }) else {
throw AccountError.invalid("Username/host may contain only letters, digits, _, ., -, and %.")
}
let account = "'\(username)'@'\(hostname)'"
switch action {
case .create, .password:
guard !password.isEmpty else { throw AccountError.invalid("Enter a password.") }
// MySQL's default SQL mode interprets backslashes as escapes.
// We check SQL mode before execution to avoid changing password meaning.
let literal = password.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "''")
let prefix = action == .create ? "CREATE USER" : "ALTER USER"
return ("\(prefix) \(account) IDENTIFIED BY '\(literal)'",
"\(prefix) \(account) IDENTIFIED BY '••••••••'")
case .lock:
return ("ALTER USER \(account) ACCOUNT LOCK", "ALTER USER \(account) ACCOUNT LOCK")
case .unlock:
return ("ALTER USER \(account) ACCOUNT UNLOCK", "ALTER USER \(account) ACCOUNT UNLOCK")
case .grant, .revoke:
guard !databaseName.isEmpty,
databaseName.unicodeScalars.allSatisfy({ allowed.contains($0) }),
privileges.contains(privilege) else {
throw AccountError.invalid("Enter a valid database name and privilege.")
}
let db = "`\(databaseName)`.*"
let command = action == .grant
? "GRANT \(privilege) ON \(db) TO \(account)"
: "REVOKE \(privilege) ON \(db) FROM \(account)"
return (command, command)
}
}
private func review() {
failure = nil
status = nil
do {
previewSQL = try sql().preview
confirm = true
} catch {
failure = error.localizedDescription
}
}
@MainActor
private func execute() async {
running = true
defer { running = false; password = "" }
do {
let command = try sql().command
let credentials = PasswordStore.read(profile.id)
if action == .create || action == .password {
let mode = try await NativeMySQLRunner.run(
profile: profile, database: "", password: credentials,
sql: "SELECT @@SESSION.sql_mode"
)
let flags = mode.rows.first?.first ?? ""
if flags.split(separator: ",").contains(where: { $0 == "NO_BACKSLASH_ESCAPES" }) {
throw AccountError.invalid("Password changes are disabled for NO_BACKSLASH_ESCAPES SQL mode.")
}
}
_ = try await NativeMySQLRunner.run(
profile: profile, database: "", password: credentials, sql: command
)
status = "\(action.rawValue) completed successfully."
failure = nil
} catch {
failure = error.localizedDescription
status = nil
}
}
private enum AccountError: LocalizedError {
case invalid(String)
var errorDescription: String? {
if case let .invalid(message) = self { return message }
return nil
}
}
}
@@ -247,7 +247,7 @@
"key": "The server did not return any columns.",
"location": {
"startingColumn": 25,
"startingLine": 1412
"startingLine": 1413
}
},
{
@@ -255,7 +255,7 @@
"key": "Loading %@…",
"location": {
"startingColumn": 30,
"startingLine": 1390
"startingLine": 1391
}
},
{
@@ -263,7 +263,7 @@
"key": "No Data",
"location": {
"startingColumn": 21,
"startingLine": 1409
"startingLine": 1410
}
},
{
@@ -271,7 +271,7 @@
"key": "Unable to Load %@",
"location": {
"startingColumn": 25,
"startingLine": 1396
"startingLine": 1397
}
},
{
@@ -279,15 +279,15 @@
"key": "Retry",
"location": {
"startingColumn": 28,
"startingLine": 1402
"startingLine": 1403
}
},
{
"comment": "",
"key": "Read-only account inspection",
"key": "Changes require confirmation",
"location": {
"startingColumn": 26,
"startingLine": 1438
"startingLine": 1444
}
},
{
@@ -295,7 +295,7 @@
"key": "Account",
"location": {
"startingColumn": 28,
"startingLine": 1422
"startingLine": 1423
}
},
{
@@ -303,7 +303,15 @@
"key": "Show Grants",
"location": {
"startingColumn": 28,
"startingLine": 1433
"startingLine": 1434
}
},
{
"comment": "",
"key": "Manage Accounts…",
"location": {
"startingColumn": 28,
"startingLine": 1438
}
},
{
@@ -311,7 +319,7 @@
"key": "Select account",
"location": {
"startingColumn": 30,
"startingLine": 1423
"startingLine": 1424
}
},
{
@@ -319,7 +327,7 @@
"key": "%@@%@",
"location": {
"startingColumn": 38,
"startingLine": 1427
"startingLine": 1428
}
},
{
@@ -327,7 +335,7 @@
"key": "Grants: %@",
"location": {
"startingColumn": 22,
"startingLine": 1462
"startingLine": 1478
}
},
{
@@ -335,7 +343,7 @@
"key": "Close",
"location": {
"startingColumn": 24,
"startingLine": 1474
"startingLine": 1490
}
},
{
@@ -343,7 +351,7 @@
"key": "Filter results",
"location": {
"startingColumn": 27,
"startingLine": 1500
"startingLine": 1516
}
},
{
@@ -351,7 +359,7 @@
"key": "Auto-refresh",
"location": {
"startingColumn": 20,
"startingLine": 1505
"startingLine": 1521
}
},
{
@@ -359,7 +367,7 @@
"key": "Refresh every five seconds",
"location": {
"startingColumn": 23,
"startingLine": 1507
"startingLine": 1523
}
},
{
@@ -367,7 +375,7 @@
"key": "Refresh",
"location": {
"startingColumn": 23,
"startingLine": 1512
"startingLine": 1528
}
},
{
@@ -375,7 +383,7 @@
"key": "SQL Editor",
"location": {
"startingColumn": 23,
"startingLine": 1519
"startingLine": 1535
}
},
{
@@ -383,7 +391,7 @@
"key": "Not loaded",
"location": {
"startingColumn": 22,
"startingLine": 1597
"startingLine": 1613
}
},
{
@@ -391,7 +399,7 @@
"key": "Updated %@",
"location": {
"startingColumn": 21,
"startingLine": 1594
"startingLine": 1610
}
},
{
@@ -399,7 +407,7 @@
"key": "%lld rows",
"location": {
"startingColumn": 18,
"startingLine": 1602
"startingLine": 1618
}
},
{
@@ -407,7 +415,7 @@
"key": "Import SQL into %@?",
"location": {
"startingColumn": 13,
"startingLine": 1904
"startingLine": 1920
}
},
{
@@ -415,7 +423,7 @@
"key": "Operation",
"location": {
"startingColumn": 20,
"startingLine": 1707
"startingLine": 1723
}
},
{
@@ -423,7 +431,7 @@
"key": "Data Export",
"location": {
"startingColumn": 22,
"startingLine": 1708
"startingLine": 1724
}
},
{
@@ -431,7 +439,7 @@
"key": "Data Import / Restore",
"location": {
"startingColumn": 22,
"startingLine": 1709
"startingLine": 1725
}
},
{
@@ -439,7 +447,7 @@
"key": "MySQL Connection",
"location": {
"startingColumn": 26,
"startingLine": 1716
"startingLine": 1732
}
},
{
@@ -447,7 +455,7 @@
"key": "Export Options",
"location": {
"startingColumn": 30,
"startingLine": 1740
"startingLine": 1756
}
},
{
@@ -455,7 +463,7 @@
"key": "Import Options",
"location": {
"startingColumn": 30,
"startingLine": 1796
"startingLine": 1812
}
},
{
@@ -463,7 +471,7 @@
"key": "Progress",
"location": {
"startingColumn": 26,
"startingLine": 1808
"startingLine": 1824
}
},
{
@@ -471,7 +479,7 @@
"key": "Connection",
"location": {
"startingColumn": 36,
"startingLine": 1718
"startingLine": 1734
}
},
{
@@ -479,7 +487,7 @@
"key": "Database",
"location": {
"startingColumn": 28,
"startingLine": 1722
"startingLine": 1738
}
},
{
@@ -487,7 +495,7 @@
"key": "Select Database",
"location": {
"startingColumn": 30,
"startingLine": 1723
"startingLine": 1739
}
},
{
@@ -495,7 +503,7 @@
"key": "Refresh Databases",
"location": {
"startingColumn": 32,
"startingLine": 1731
"startingLine": 1747
}
},
{
@@ -503,7 +511,7 @@
"key": "No tables found. Select a database or refresh the list.",
"location": {
"startingColumn": 34,
"startingLine": 1762
"startingLine": 1778
}
},
{
@@ -511,7 +519,7 @@
"key": "InnoDB data is exported using a consistent snapshot and a single streamed query per table. Tables without unique indexes are supported.",
"location": {
"startingColumn": 30,
"startingLine": 1791
"startingLine": 1807
}
},
{
@@ -519,7 +527,7 @@
"key": "Table structure",
"location": {
"startingColumn": 32,
"startingLine": 1742
"startingLine": 1758
}
},
{
@@ -527,7 +535,7 @@
"key": "Table data",
"location": {
"startingColumn": 32,
"startingLine": 1743
"startingLine": 1759
}
},
{
@@ -535,7 +543,7 @@
"key": "Add DROP TABLE IF EXISTS",
"location": {
"startingColumn": 32,
"startingLine": 1744
"startingLine": 1760
}
},
{
@@ -543,7 +551,7 @@
"key": "Include views (definitions)",
"location": {
"startingColumn": 32,
"startingLine": 1746
"startingLine": 1762
}
},
{
@@ -551,7 +559,7 @@
"key": "Include triggers",
"location": {
"startingColumn": 32,
"startingLine": 1748
"startingLine": 1764
}
},
{
@@ -559,7 +567,7 @@
"key": "Include stored procedures and functions",
"location": {
"startingColumn": 32,
"startingLine": 1750
"startingLine": 1766
}
},
{
@@ -567,7 +575,7 @@
"key": "Loading tables…",
"location": {
"startingColumn": 57,
"startingLine": 1760
"startingLine": 1776
}
},
{
@@ -575,7 +583,7 @@
"key": "Tables (%lld selected)",
"location": {
"startingColumn": 34,
"startingLine": 1753
"startingLine": 1769
}
},
{
@@ -583,7 +591,7 @@
"key": "All",
"location": {
"startingColumn": 36,
"startingLine": 1755
"startingLine": 1771
}
},
{
@@ -591,7 +599,7 @@
"key": "None",
"location": {
"startingColumn": 36,
"startingLine": 1756
"startingLine": 1772
}
},
{
@@ -599,7 +607,7 @@
"key": "Refresh",
"location": {
"startingColumn": 36,
"startingLine": 1757
"startingLine": 1773
}
},
{
@@ -607,7 +615,7 @@
"key": "Views (%lld selected)",
"location": {
"startingColumn": 38,
"startingLine": 1776
"startingLine": 1792
}
},
{
@@ -615,7 +623,7 @@
"key": "All",
"location": {
"startingColumn": 40,
"startingLine": 1778
"startingLine": 1794
}
},
{
@@ -623,7 +631,7 @@
"key": "None",
"location": {
"startingColumn": 40,
"startingLine": 1779
"startingLine": 1795
}
},
{
@@ -631,7 +639,7 @@
"key": "Import SQL statements into the selected database.",
"location": {
"startingColumn": 30,
"startingLine": 1798
"startingLine": 1814
}
},
{
@@ -639,7 +647,7 @@
"key": "Supports MySQL DELIMITER directives, triggers and stored routines. Import is not transactional; use a verified backup before restoring.",
"location": {
"startingColumn": 30,
"startingLine": 1801
"startingLine": 1817
}
},
{
@@ -647,7 +655,7 @@
"key": "Current table: %@ / %@ rows · Total exported: %@",
"location": {
"startingColumn": 30,
"startingLine": 1825
"startingLine": 1841
}
},
{
@@ -655,7 +663,7 @@
"key": "Close",
"location": {
"startingColumn": 24,
"startingLine": 1839
"startingLine": 1855
}
},
{
@@ -663,7 +671,7 @@
"key": "Verify Backup…",
"location": {
"startingColumn": 28,
"startingLine": 1847
"startingLine": 1863
}
},
{
@@ -671,7 +679,7 @@
"key": "Verify an SQL file against its SHA-256 checksum sidecar",
"location": {
"startingColumn": 27,
"startingLine": 1850
"startingLine": 1866
}
},
{
@@ -679,7 +687,7 @@
"key": "Verify Restored Database…",
"location": {
"startingColumn": 28,
"startingLine": 1851
"startingLine": 1867
}
},
{
@@ -687,7 +695,7 @@
"key": "Compare restored table row counts and object names with the export manifest",
"location": {
"startingColumn": 27,
"startingLine": 1855
"startingLine": 1871
}
},
{
@@ -695,7 +703,7 @@
"key": "Cancel",
"location": {
"startingColumn": 28,
"startingLine": 1859
"startingLine": 1875
}
},
{
@@ -703,7 +711,7 @@
"key": "Export SQL…",
"location": {
"startingColumn": 31,
"startingLine": 1866
"startingLine": 1882
}
},
{
@@ -711,7 +719,7 @@
"key": "Import SQL…",
"location": {
"startingColumn": 31,
"startingLine": 1867
"startingLine": 1883
}
},
{
@@ -719,7 +727,7 @@
"key": "Import SQL",
"location": {
"startingColumn": 20,
"startingLine": 1907
"startingLine": 1923
}
},
{
@@ -727,7 +735,7 @@
"key": "Cancel",
"location": {
"startingColumn": 20,
"startingLine": 1915
"startingLine": 1931
}
},
{
@@ -735,7 +743,7 @@
"key": "SQL files can modify or delete existing database objects and data. Ensure you have a backup before continuing.",
"location": {
"startingColumn": 17,
"startingLine": 1920
"startingLine": 1936
}
},
{
@@ -743,7 +751,7 @@
"key": "Database Import / Export",
"location": {
"startingColumn": 22,
"startingLine": 1932
"startingLine": 1948
}
},
{
@@ -751,7 +759,119 @@
"key": "Built-in native MySQL database tools",
"location": {
"startingColumn": 22,
"startingLine": 1935
"startingLine": 1951
}
},
{
"comment": "",
"key": "Confirm %@",
"location": {
"startingColumn": 16,
"startingLine": 3002
}
},
{
"comment": "",
"key": "Manage MySQL Accounts",
"location": {
"startingColumn": 18,
"startingLine": 2952
}
},
{
"comment": "",
"key": "Account changes run on %@. Review the SQL before applying it.",
"location": {
"startingColumn": 18,
"startingLine": 2953
}
},
{
"comment": "",
"key": "Operation",
"location": {
"startingColumn": 24,
"startingLine": 2956
}
},
{
"comment": "",
"key": "Username",
"location": {
"startingColumn": 27,
"startingLine": 2961
}
},
{
"comment": "",
"key": "Host",
"location": {
"startingColumn": 27,
"startingLine": 2963
}
},
{
"comment": "",
"key": "Password",
"location": {
"startingColumn": 33,
"startingLine": 2965
}
},
{
"comment": "",
"key": "Database",
"location": {
"startingColumn": 31,
"startingLine": 2968
}
},
{
"comment": "",
"key": "Privilege",
"location": {
"startingColumn": 28,
"startingLine": 2969
}
},
{
"comment": "",
"key": "Close",
"location": {
"startingColumn": 24,
"startingLine": 2984
}
},
{
"comment": "",
"key": "Review SQL…",
"location": {
"startingColumn": 24,
"startingLine": 2986
}
},
{
"comment": "",
"key": "Cancel",
"location": {
"startingColumn": 20,
"startingLine": 3003
}
},
{
"comment": "",
"key": "Apply Change",
"location": {
"startingColumn": 20,
"startingLine": 3004
}
},
{
"comment": "",
"key": "Run this operation against %@?\n\n%@",
"location": {
"startingColumn": 18,
"startingLine": 3009
}
}
]
@@ -19,3 +19,4 @@
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
@@ -19,7 +19,7 @@
<dict>
<key>cdhash</key>
<data>
lXsCGsMbs4xf0Myx5xVMRot9mP8=
AZIB1Fjt6RqKV00p8DK8RGVfw64=
</data>
<key>requirement</key>
<string>identifier "WorkbenchReplacement.debug" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: Mark Gregory (FLBRJ3BYDD)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */</string>
+42 -42
View File
@@ -48,48 +48,6 @@
<key>uniqueIdentifier</key>
<string>2FC9B29D-EE21-4701-8634-B8C65E3A79A5</string>
</dict>
<key>B2C949DC-0503-4157-BC15-F74641625B50</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
<key>documentTypeString</key>
<string>&lt;nil&gt;</string>
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>B2C949DC-0503-4157-BC15-F74641625B50.xcactivitylog</string>
<key>hasPrimaryLog</key>
<true/>
<key>primaryObservable</key>
<dict>
<key>highLevelStatus</key>
<string>S</string>
<key>totalNumberOfAnalyzerIssues</key>
<integer>0</integer>
<key>totalNumberOfErrors</key>
<integer>0</integer>
<key>totalNumberOfTestFailures</key>
<integer>0</integer>
<key>totalNumberOfWarnings</key>
<integer>0</integer>
</dict>
<key>schemeIdentifier-containerName</key>
<string>WorkbenchReplacement project</string>
<key>schemeIdentifier-schemeName</key>
<string>WorkbenchReplacement</string>
<key>schemeIdentifier-sharedScheme</key>
<integer>1</integer>
<key>signature</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>timeStartedRecording</key>
<real>813877672.07052696</real>
<key>timeStoppedRecording</key>
<real>813877685.02004802</real>
<key>title</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>uniqueIdentifier</key>
<string>B2C949DC-0503-4157-BC15-F74641625B50</string>
</dict>
<key>B8940B11-E198-4118-908A-6DCC06A473A1</key>
<dict>
<key>className</key>
@@ -132,6 +90,48 @@
<key>uniqueIdentifier</key>
<string>B8940B11-E198-4118-908A-6DCC06A473A1</string>
</dict>
<key>BE675D45-226B-48D5-9BED-8FCC12B530FF</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
<key>documentTypeString</key>
<string>&lt;nil&gt;</string>
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>BE675D45-226B-48D5-9BED-8FCC12B530FF.xcactivitylog</string>
<key>hasPrimaryLog</key>
<true/>
<key>primaryObservable</key>
<dict>
<key>highLevelStatus</key>
<string>S</string>
<key>totalNumberOfAnalyzerIssues</key>
<integer>0</integer>
<key>totalNumberOfErrors</key>
<integer>0</integer>
<key>totalNumberOfTestFailures</key>
<integer>0</integer>
<key>totalNumberOfWarnings</key>
<integer>0</integer>
</dict>
<key>schemeIdentifier-containerName</key>
<string>WorkbenchReplacement project</string>
<key>schemeIdentifier-schemeName</key>
<string>WorkbenchReplacement</string>
<key>schemeIdentifier-sharedScheme</key>
<integer>1</integer>
<key>signature</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>timeStartedRecording</key>
<real>813878365.00770497</real>
<key>timeStoppedRecording</key>
<real>813878379.89619505</real>
<key>title</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>uniqueIdentifier</key>
<string>BE675D45-226B-48D5-9BED-8FCC12B530FF</string>
</dict>
<key>D705A532-F8F7-4B72-94B4-3464B760E3E3</key>
<dict>
<key>className</key>
@@ -1 +1 @@
1792185392.568994: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
1792185565.3857698: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
+1 -1
View File
@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>LastAccessedDate</key>
<date>2026-10-16T21:16:32Z</date>
<date>2026-10-16T21:19:25Z</date>
<key>WorkspacePath</key>
<string>/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement.xcodeproj</string>
</dict>