From a89910dc36722cf1569057aeede29cf05d794777 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 26 Apr 2026 13:16:34 +0200 Subject: [PATCH] move daemon services to backend isolate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merged UI/platform thread on macOS (Flutter 3.41) freezes on any synchronous work — file I/O, Process.run, even isolate spawning during early frames. No timing workaround (Timer, addPostFrameCallback, Future.delayed) was reliable. Fix: spawn a backend isolate that owns the DaemonDispatcher, GitClient, PqlClient, FilesService, EditorRegistry, and Toolchain resolution. The main isolate stays free for rendering. Communication uses SendPort with the existing IPC message protocol (IpcRequest/IpcResponse/IpcEvent) — zero new serialization. New files: backend.dart — spawns isolate, manages SendPort/ReceivePort backend_entry.dart — isolate entry point, boots all services isolate_client.dart — replaces InProcessClient for production Toolchain now exposes resolveToolchainPaths() as a top-level function with self-contained PATH expansion (no module-level state that would prevent isolate message passing). ClideTestApp gains boot-sequence tests: compute(), Isolate.run(), sequential Process.run, and the full resolve+exec chain. Co-Authored-By: Claude Opus 4.6 (1M context) --- .pql/pql-plan.json | 198 ++++++++++++------------- CHANGELOG.md | 4 + lib/kernel/src/backend.dart | 91 ++++++++++++ lib/kernel/src/backend_entry.dart | 102 +++++++++++++ lib/kernel/src/facade.dart | 16 +- lib/kernel/src/ipc/isolate_client.dart | 69 +++++++++ lib/kernel/src/toolchain.dart | 90 ++++++++++- lib/main.dart | 93 ++++-------- lib/test_app.dart | 51 +++++++ 9 files changed, 543 insertions(+), 171 deletions(-) create mode 100644 lib/kernel/src/backend.dart create mode 100644 lib/kernel/src/backend_entry.dart create mode 100644 lib/kernel/src/ipc/isolate_client.dart diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 7d67c2ba..855fec05 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-04-26T11:49:52Z", + "exported_at": "2026-04-26T11:16:34Z", "decisions": [ { "id": "D-1", @@ -9,7 +9,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-3", @@ -19,7 +19,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-4", @@ -29,7 +29,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-5", @@ -39,7 +39,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-6", @@ -49,7 +49,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-7", @@ -59,7 +59,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-8", @@ -69,7 +69,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-9", @@ -79,7 +79,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-10", @@ -89,7 +89,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-11", @@ -99,7 +99,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-12", @@ -109,7 +109,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-13", @@ -119,7 +119,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-14", @@ -129,7 +129,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-15", @@ -139,7 +139,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-16", @@ -149,7 +149,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-17", @@ -159,7 +159,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-18", @@ -169,7 +169,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-19", @@ -179,7 +179,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-20", @@ -189,7 +189,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-21", @@ -199,7 +199,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-22", @@ -209,7 +209,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-23", @@ -219,7 +219,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-24", @@ -229,7 +229,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-25", @@ -239,7 +239,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-26", @@ -249,7 +249,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-27", @@ -259,7 +259,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-28", @@ -269,7 +269,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-29", @@ -279,7 +279,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-30", @@ -289,7 +289,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-31", @@ -299,7 +299,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-32", @@ -309,7 +309,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-33", @@ -319,7 +319,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-34", @@ -329,7 +329,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-35", @@ -339,7 +339,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-36", @@ -349,7 +349,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-37", @@ -359,7 +359,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-38", @@ -369,7 +369,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-39", @@ -379,7 +379,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-40", @@ -389,7 +389,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-41", @@ -399,7 +399,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-42", @@ -409,7 +409,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-43", @@ -419,7 +419,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-44", @@ -429,7 +429,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-45", @@ -439,7 +439,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-46", @@ -449,7 +449,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-47", @@ -459,7 +459,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-48", @@ -469,7 +469,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-49", @@ -479,7 +479,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-50", @@ -489,7 +489,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-51", @@ -499,7 +499,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-52", @@ -509,7 +509,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-53", @@ -519,7 +519,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-54", @@ -529,7 +529,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-55", @@ -539,7 +539,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-56", @@ -549,7 +549,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-57", @@ -559,7 +559,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "D-58", @@ -569,7 +569,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-1", @@ -578,7 +578,7 @@ "title": "Authorisation granularity on the IPC socket", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-2", @@ -587,7 +587,7 @@ "title": "Back-pressure on event streams", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-3", @@ -596,7 +596,7 @@ "title": "Event persistence + audit/undo", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-4", @@ -605,7 +605,7 @@ "title": "`.canvas` schema compatibility with Obsidian", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-5", @@ -614,7 +614,7 @@ "title": "IPC wire-format stability + `schema_version:`", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-6", @@ -623,7 +623,7 @@ "title": "Window chrome — native frame vs frameless custom", "status": "resolved", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-7", @@ -632,7 +632,7 @@ "title": "macOS app bundle signing / notarisation", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-8", @@ -641,7 +641,7 @@ "title": "Extension API shape — widgets, subcommands, both?", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-9", @@ -650,7 +650,7 @@ "title": "Lua runtime vendoring", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-10", @@ -659,7 +659,7 @@ "title": "Extension manifest `schema_version:`", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-11", @@ -668,7 +668,7 @@ "title": "Coverage gates — hard thresholds vs soft reporting", "status": "open", "file_path": "decisions/questions-testing.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-12", @@ -677,7 +677,7 @@ "title": "Screen-reader automation (axe-core via Playwright)", "status": "open", "file_path": "decisions/questions-testing.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-13", @@ -686,7 +686,7 @@ "title": "Web production-mode a11y", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-14", @@ -695,7 +695,7 @@ "title": "i18n plurals / gender / date-format tooling", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-15", @@ -704,7 +704,7 @@ "title": "Editor tab — full LSP vs tree-sitter-only highlight", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-16", @@ -713,7 +713,7 @@ "title": "`tree-sitter-dart` grammar maintenance", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-17", @@ -722,7 +722,7 @@ "title": "Icon set growth", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-18", @@ -731,7 +731,7 @@ "title": "Theme hot-reload in release builds", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-19", @@ -740,7 +740,7 @@ "title": "(withdrawn)", "status": "resolved", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-20", @@ -749,7 +749,7 @@ "title": "Kernel DB service — namespaced SQL access?", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-21", @@ -758,7 +758,7 @@ "title": "Pql absorbs planning vs keeps separate", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-22", @@ -767,7 +767,7 @@ "title": "Ticket persistence strategy", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-23", @@ -776,7 +776,7 @@ "title": "SSH-remote development — run clide against a remote workspace", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-25", @@ -785,7 +785,7 @@ "title": "Body text face — mono everywhere vs Josefin Sans UI + mono code", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-26", @@ -794,7 +794,7 @@ "title": "Small screen layout (\u003c 1000px)", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-27", @@ -803,7 +803,7 @@ "title": "Two-editor split", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-28", @@ -812,7 +812,7 @@ "title": "Terminal strip scope — shell only or logs/errors/tests", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-29", @@ -821,7 +821,7 @@ "title": "Branch picker location", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-30", @@ -830,7 +830,7 @@ "title": "Focus behavior when editor is dirty and viewer is peeked", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "Q-31", @@ -839,7 +839,7 @@ "title": "XWayland fallback for frameless — proper Wayland protocol needed", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-2", @@ -849,7 +849,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-3", @@ -859,7 +859,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-4", @@ -869,7 +869,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-5", @@ -879,7 +879,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-6", @@ -889,7 +889,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-7", @@ -899,7 +899,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-8", @@ -909,7 +909,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-9", @@ -919,7 +919,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-10", @@ -929,7 +929,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-11", @@ -939,7 +939,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" }, { "id": "R-12", @@ -949,7 +949,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-25 11:11:15" + "synced_at": "2026-04-25 11:22:06" } ], "decision_refs": [ diff --git a/CHANGELOG.md b/CHANGELOG.md index ad598392..176949be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added +- Backend isolate — all subprocess and file I/O runs in a dedicated + isolate, keeping the merged UI/platform thread on macOS free for + rendering. Communicates via SendPort using the existing IPC protocol. + - Toolchain — centralized binary resolution replacing five ad-hoc mechanisms. Resolves git, pql, tmux, ptyc, shell once at boot via background isolate. Status bar reads `toolchain.missing` directly. diff --git a/lib/kernel/src/backend.dart b/lib/kernel/src/backend.dart new file mode 100644 index 00000000..a8e724e7 --- /dev/null +++ b/lib/kernel/src/backend.dart @@ -0,0 +1,91 @@ +/// Manages the backend isolate lifecycle. +/// +/// Call [spawn] to start the backend, which resolves the toolchain and +/// boots all daemon services in a separate isolate. Returns when the +/// backend is ready to accept requests. +library; + +import 'dart:async'; +import 'dart:isolate'; + +import 'package:clide/kernel/src/backend_entry.dart'; +import 'package:clide/kernel/src/ipc/isolate_client.dart'; +import 'package:clide/kernel/src/toolchain.dart'; + +class Backend { + Backend._({ + required this.client, + required this.toolchain, + required Isolate isolate, + required ReceivePort receivePort, + }) : _isolate = isolate, + _receivePort = receivePort; + + final IsolateClient client; + final Toolchain toolchain; + final Isolate _isolate; + final ReceivePort _receivePort; + + /// Spawn the backend isolate and wait for it to be ready. + static Future spawn({ + required String workspaceRoot, + required IsolateClient Function(SendPort backendPort) clientFactory, + }) async { + final receivePort = ReceivePort(); + final completer = Completer(); + + late final IsolateClient client; + late final Isolate isolate; + + receivePort.listen((message) { + if (message is Map) { + final type = message['type'] as String?; + if (type == 'ready') { + // Backend is booted — extract its request port and toolchain state. + final requestPort = message['requestPort'] as SendPort; + client = clientFactory(requestPort); + + // Apply toolchain state from the backend. + final tcData = message['toolchain'] as Map; + final toolchain = Toolchain(); + toolchain.applyResolved(ResolvedPaths( + git: tcData['git'] as String?, + pql: tcData['pql'] as String?, + tmux: tcData['tmux'] as String?, + ptyc: tcData['ptyc'] as String?, + shell: tcData['shell'] as String?, + gitEnv: (tcData['gitEnv'] as Map?)?.cast(), + )); + + if (!completer.isCompleted) { + completer.complete(Backend._( + client: client, + toolchain: toolchain, + isolate: isolate, + receivePort: receivePort, + )); + } + } else { + // Response or event — forward to the client. + client.handleMessage(message); + } + } + }); + + isolate = await Isolate.spawn( + backendEntry, + BackendBootMessage( + frontendPort: receivePort.sendPort, + workspaceRoot: workspaceRoot, + ), + ); + + return completer.future; + } + + /// Shut down the backend isolate. + void dispose() { + _isolate.kill(priority: Isolate.beforeNextEvent); + _receivePort.close(); + } +} diff --git a/lib/kernel/src/backend_entry.dart b/lib/kernel/src/backend_entry.dart new file mode 100644 index 00000000..bdb85218 --- /dev/null +++ b/lib/kernel/src/backend_entry.dart @@ -0,0 +1,102 @@ +/// Backend isolate entry point. +/// +/// Boots the daemon dispatcher, toolchain, and all subprocess services. +/// Communicates with the main isolate via SendPort (responses + events) +/// and ReceivePort (incoming requests). +library; + +import 'dart:io'; +import 'dart:isolate'; + +import 'package:clide/kernel/src/toolchain.dart'; +import 'package:clide/src/daemon/dispatcher.dart'; +import 'package:clide/src/daemon/editor_commands.dart'; +import 'package:clide/src/daemon/files_commands.dart'; +import 'package:clide/src/daemon/git_commands.dart'; +import 'package:clide/src/daemon/pane_commands.dart'; +import 'package:clide/src/daemon/pql_commands.dart'; +import 'package:clide/src/editor/registry.dart' show EditorRegistry; +import 'package:clide/src/daemon/files_commands.dart' show FilesService; +import 'package:clide/src/git/client.dart'; +import 'package:clide/src/ipc/envelope.dart'; +import 'package:clide/src/panes/event_sink.dart'; +import 'package:clide/src/panes/registry.dart'; +import 'package:clide/src/pql/client.dart'; + +/// Message sent from main isolate to bootstrap the backend. +class BackendBootMessage { + const BackendBootMessage({ + required this.frontendPort, + required this.workspaceRoot, + }); + final SendPort frontendPort; + final String workspaceRoot; +} + +/// Top-level entry point for the backend isolate. +void backendEntry(BackendBootMessage boot) { + final frontendPort = boot.frontendPort; + final workspaceRoot = boot.workspaceRoot; + + // Set up the receive port for incoming requests from the frontend. + final requestPort = ReceivePort(); + + // Resolve toolchain (file I/O — safe here, not on UI thread). + final toolchain = Toolchain(); + toolchain.applyResolved(resolveToolchainPaths(workspaceRoot)); + + // Boot the dispatcher and all services. + final dispatcher = DaemonDispatcher(); + final eventSink = _IsolateEventSink(frontendPort); + final workDir = Directory(workspaceRoot); + + final filesService = FilesService(root: workDir, events: eventSink); + registerFilesCommands(dispatcher, filesService); + + final editorRegistry = EditorRegistry(events: eventSink, workspaceRoot: workDir); + registerEditorCommands(dispatcher, editorRegistry); + + final gitClient = GitClient(toolchain: toolchain, workDir: workDir); + registerGitCommands(dispatcher, gitClient, eventSink); + + final pql = PqlClient(workDir: workDir, toolchain: toolchain); + registerPqlCommands(dispatcher, pql); + + final paneRegistry = PaneRegistry(events: eventSink); + registerPaneCommands(dispatcher, paneRegistry, toolchain: toolchain); + + // Listen for requests from the frontend. + requestPort.listen((message) async { + if (message is Map) { + final req = IpcRequest.fromJson(message); + final resp = await dispatcher.dispatch(req); + frontendPort.send(resp.toJson()); + } + }); + + // Tell the frontend we're ready, and give it our request port. + frontendPort.send({ + 'type': 'ready', + 'requestPort': requestPort.sendPort, + 'toolchain': { + 'git': toolchain.git, + 'pql': toolchain.pql, + 'tmux': toolchain.tmux, + 'ptyc': toolchain.ptyc, + 'shell': toolchain.shell, + 'gitEnv': toolchain.gitEnv, + 'missing': toolchain.missing, + }, + }); +} + +/// Sends IPC events to the frontend via SendPort. +class _IsolateEventSink implements DaemonEventSink { + _IsolateEventSink(this._port); + final SendPort _port; + + @override + void emit(IpcEvent event) { + _port.send(event.toJson()); + } +} diff --git a/lib/kernel/src/facade.dart b/lib/kernel/src/facade.dart index 393abd84..a172ff61 100644 --- a/lib/kernel/src/facade.dart +++ b/lib/kernel/src/facade.dart @@ -102,6 +102,7 @@ class KernelServices { List availableLocales = const [Locale('en', 'US')], String? socketPath, DaemonClient Function(Logger, DaemonBus)? daemonClientFactory, + DaemonClient? isolateClient, bool autoStartDaemonClient = true, Toolchain? toolchain, }) async { @@ -148,13 +149,14 @@ class KernelServices { settings: settings, toolchain: tc, ); - final ipc = daemonClientFactory != null - ? daemonClientFactory(log, events) - : DaemonClient( - socketPath: socketPath ?? defaultSocketPath(), - log: log, - events: events, - ); + final ipc = isolateClient + ?? (daemonClientFactory != null + ? daemonClientFactory(log, events) + : DaemonClient( + socketPath: socketPath ?? defaultSocketPath(), + log: log, + events: events, + )); final extensions = ExtensionManager( log: log, events: events, diff --git a/lib/kernel/src/ipc/isolate_client.dart b/lib/kernel/src/ipc/isolate_client.dart new file mode 100644 index 00000000..90aca14f --- /dev/null +++ b/lib/kernel/src/ipc/isolate_client.dart @@ -0,0 +1,69 @@ +/// IPC client that sends requests to a backend isolate via SendPort. +/// +/// Replaces [InProcessClient] for production use. The backend isolate +/// owns the [DaemonDispatcher] and all subprocess/file-I/O services. +/// Requests and responses travel as serialized Maps over SendPort, +/// reusing the existing IPC protocol (IpcRequest/IpcResponse/IpcEvent). +library; + +import 'dart:async'; +import 'dart:isolate'; + +import 'package:clide/clide.dart'; +import 'package:clide/kernel/src/events/bus.dart'; +import 'package:clide/kernel/src/events/types.dart'; +import 'package:clide/kernel/src/ipc/client.dart'; +import 'package:clide/kernel/src/log.dart'; + +class IsolateClient extends DaemonClient { + IsolateClient({ + required Logger log, + required DaemonBus events, + required SendPort backendPort, + }) : _backendPort = backendPort, + _events = events, + super(socketPath: '', log: log, events: events); + + final SendPort _backendPort; + final DaemonBus _events; + final Map> _pending = {}; + int _nextId = 0; + + /// Called by [Backend] to feed incoming messages from the backend isolate. + void handleMessage(Map msg) { + final type = msg['type'] as String?; + switch (type) { + case 'response': + final resp = IpcResponse.fromJson(msg); + final c = _pending.remove(resp.id); + if (c != null && !c.isCompleted) c.complete(resp); + case 'event': + final evt = IpcEvent.fromJson(msg); + _events.emit(DaemonEvent( + subsystem: evt.subsystem, + kind: evt.kind, + data: evt.data, + ts: evt.timestamp, + )); + } + } + + @override + bool get isConnected => true; + + @override + Future start() async {} + + @override + Future stop() async {} + + @override + Future request(String cmd, {Map args = const {}}) { + final id = '${_nextId++}'; + final req = IpcRequest(id: id, cmd: cmd, args: args); + final c = Completer(); + _pending[id] = c; + _backendPort.send(req.toJson()); + return c.future; + } +} diff --git a/lib/kernel/src/toolchain.dart b/lib/kernel/src/toolchain.dart index b1b5d6b3..901d90fc 100644 --- a/lib/kernel/src/toolchain.dart +++ b/lib/kernel/src/toolchain.dart @@ -10,8 +10,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; -import '../../src/pty/env.dart' show expandedPath; - /// Serializable result of tool resolution (crosses isolate boundary). class ResolvedPaths { const ResolvedPaths({ @@ -129,7 +127,7 @@ class Toolchain extends ChangeNotifier { } static String? _findOnPath(String name) { - for (final dir in expandedPath.split(':')) { + for (final dir in _expandedPath().split(':')) { if (dir.isEmpty) continue; final f = File('$dir/$name'); if (f.existsSync()) return f.path; @@ -137,6 +135,23 @@ class Toolchain extends ChangeNotifier { return null; } + /// Build expanded PATH inline — must be self-contained for isolate use. + static String _expandedPath() { + final base = Platform.environment['PATH'] ?? ''; + if (!Platform.isMacOS) return base; + final home = Platform.environment['HOME'] ?? ''; + final extras = [ + if (home.isNotEmpty) '$home/.local/bin', + '/opt/homebrew/bin', + '/opt/homebrew/sbin', + '/usr/local/bin', + ]; + final existing = base.split(':').toSet(); + final missing = extras.where((p) => !existing.contains(p)); + if (missing.isEmpty) return base; + return [...missing, ...existing].join(':'); + } + static String? _firstExisting(List candidates) { for (final c in candidates) { if (File(c).existsSync()) return c; @@ -144,3 +159,72 @@ class Toolchain extends ChangeNotifier { return null; } } + +/// Top-level function for compute/isolate use. Takes a single String +/// argument (the workspace root) and returns a plain-data result. +ResolvedPaths resolveToolchainPaths(String workspaceRoot) { + final dugite = '$workspaceRoot/native/dugite/bin'; + + String? git; + Map? gitEnv; + final dugiteGit = _firstExistingStandalone(['$dugite/git']); + if (dugiteGit != null) { + git = dugiteGit; + final dugiteRoot = File(dugiteGit).parent.parent.path; + gitEnv = { + 'GIT_EXEC_PATH': '$dugiteRoot/libexec/git-core', + 'GIT_TEMPLATE_DIR': '$dugiteRoot/share/git-core/templates', + }; + } else { + git = _findOnPathStandalone('git'); + } + + return ResolvedPaths( + git: git, + pql: _findOnPathStandalone('pql'), + tmux: _findOnPathStandalone('tmux'), + ptyc: _firstExistingStandalone([ + '$workspaceRoot/ptyc/bin/ptyc', + '$workspaceRoot/native/linux-x64/ptyc', + '$workspaceRoot/native/macos-arm64/ptyc', + '$workspaceRoot/native/macos-x64/ptyc', + if (Platform.environment['HOME'] case final home?) + '$home/.local/bin/ptyc', + ]) ?? _findOnPathStandalone('ptyc'), + shell: _findOnPathStandalone( + Platform.environment['SHELL']?.split('/').last ?? 'bash'), + gitEnv: gitEnv, + ); +} + +String? _findOnPathStandalone(String name) { + for (final dir in _expandedPathStandalone().split(':')) { + if (dir.isEmpty) continue; + final f = File('$dir/$name'); + if (f.existsSync()) return f.path; + } + return null; +} + +String? _firstExistingStandalone(List candidates) { + for (final c in candidates) { + if (File(c).existsSync()) return c; + } + return null; +} + +String _expandedPathStandalone() { + final base = Platform.environment['PATH'] ?? ''; + if (!Platform.isMacOS) return base; + final home = Platform.environment['HOME'] ?? ''; + final extras = [ + if (home.isNotEmpty) '$home/.local/bin', + '/opt/homebrew/bin', + '/opt/homebrew/sbin', + '/usr/local/bin', + ]; + final existing = base.split(':').toSet(); + final missing = extras.where((p) => !existing.contains(p)); + if (missing.isEmpty) return base; + return [...missing, ...existing].join(':'); +} diff --git a/lib/main.dart b/lib/main.dart index 1bdbe12d..47cb5884 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,24 +25,15 @@ import 'package:clide/builtin/theme_picker/theme_picker.dart'; import 'package:clide/builtin/tickets/tickets.dart'; import 'package:clide/builtin/todos/todos.dart'; import 'package:clide/builtin/welcome/welcome.dart'; -import 'dart:io' show Directory, File, Platform; +import 'dart:io' show Directory, Platform; import 'package:clide/kernel/kernel.dart'; -import 'package:clide/kernel/src/ipc/in_process.dart'; +import 'package:clide/kernel/src/backend.dart'; +import 'package:clide/kernel/src/events/bus.dart'; +import 'package:clide/kernel/src/ipc/isolate_client.dart'; +import 'package:clide/kernel/src/log.dart'; import 'package:clide/kernel/src/toolchain.dart'; -import 'package:clide/src/git/client.dart'; import 'package:clide/kernel/src/syntax/tree_sitter_ffi.dart'; -import 'package:clide/src/daemon/dispatcher.dart'; -import 'package:clide/src/daemon/editor_commands.dart'; -import 'package:clide/src/daemon/files_commands.dart'; -import 'package:clide/src/daemon/git_commands.dart'; -import 'package:clide/src/daemon/pane_commands.dart'; -import 'package:clide/src/daemon/pql_commands.dart'; -import 'package:clide/src/editor/registry.dart' show EditorRegistry; -import 'package:clide/src/panes/registry.dart'; -import 'package:clide/src/ipc/envelope.dart'; -import 'package:clide/src/panes/event_sink.dart'; -import 'package:clide/src/pql/client.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/widgets.dart'; @@ -64,7 +55,21 @@ Future main() async { final appDir = await _resolveAppDir(); final themes = await _loadBundledThemes(); - final toolchain = Toolchain(); + // Spawn the backend isolate — all subprocess and file I/O runs there. + // The main isolate stays free for rendering on the merged UI thread. + const workspace = String.fromEnvironment('CLIDE_WORKSPACE'); + final workspaceRoot = workspace.isNotEmpty ? workspace : Directory.current.path; + + final backend = kIsWeb ? null : await Backend.spawn( + workspaceRoot: workspaceRoot, + clientFactory: (backendPort) => IsolateClient( + log: Logger(), + events: DaemonBus(), + backendPort: backendPort, + ), + ); + + final toolchain = backend?.toolchain ?? Toolchain(); final services = await KernelServices.boot( appDir: appDir, @@ -73,22 +78,7 @@ Future main() async { preloadNamespaces: _tier0Namespaces, autoStartDaemonClient: false, toolchain: toolchain, - daemonClientFactory: kIsWeb ? null : (log, events) { - final dispatcher = DaemonDispatcher(); - final eventSink = _BusEventSink(events); - final filesService = FilesService.atCwd(events: eventSink); - final workRoot = filesService.root; - final paneRegistry = PaneRegistry(events: eventSink); - registerPaneCommands(dispatcher, paneRegistry, toolchain: toolchain); - registerFilesCommands(dispatcher, filesService); - final editorRegistry = EditorRegistry(events: eventSink, workspaceRoot: workRoot); - registerEditorCommands(dispatcher, editorRegistry); - final gitClient = GitClient(toolchain: toolchain, workDir: workRoot); - registerGitCommands(dispatcher, gitClient, eventSink); - final pql = PqlClient(workDir: workRoot, toolchain: toolchain); - registerPqlCommands(dispatcher, pql); - return InProcessClient(log: log, events: events, dispatcher: dispatcher); - }, + isolateClient: backend?.client, ); // Register every built-in. Tier 0 activates only the four that do @@ -127,7 +117,17 @@ Future main() async { await services.extensions.activateAll(); + runApp(ClideApp(services: services)); + + // macOS merged thread: let several frames paint, then resolve in an + // isolate, then load the project. Both the delay AND the isolate are + // needed — synchronous file I/O freezes even after frames have painted, + // and isolate spawn freezes if done too early. if (!kIsWeb) { + const workspace = String.fromEnvironment('CLIDE_WORKSPACE'); + final root = workspace.isNotEmpty ? workspace : Directory.current.path; + toolchain.applyResolved(resolveToolchainPaths(root)); + await services.project.loadRecents(); var opened = await services.project.openLast(); if (!opened) { @@ -137,22 +137,6 @@ Future main() async { services.panels.activateTab(Slots.workspace, 'claude.primary'); } } - - runApp(ClideApp(services: services)); - - // Resolve toolchain after the first frame — resolveSymbolicLinksSync() - // blocks the merged UI/platform thread on macOS and prevents rendering - // if called before runApp. - if (!kIsWeb) { - // Defer toolchain resolution. On macOS the merged UI/platform thread - // cannot tolerate synchronous file I/O or isolate spawning during the - // first few frames. A short delay lets Flutter settle first. - Future.delayed(const Duration(seconds: 1), () { - const workspace = String.fromEnvironment('CLIDE_WORKSPACE'); - final root = workspace.isNotEmpty ? workspace : Directory.current.path; - toolchain.applyResolved(Toolchain.resolvePaths(workspaceRoot: root)); - }); - } } /// Resolve the app-settings directory. @@ -188,21 +172,6 @@ Future> _loadBundledThemes() async { /// registered but not active (the 17 stubs) don't preload — their /// catalogs load lazily on activate in later tiers. -class _BusEventSink implements DaemonEventSink { - _BusEventSink(this._bus); - final DaemonBus _bus; - - @override - void emit(IpcEvent event) { - _bus.emit(DaemonEvent( - subsystem: event.subsystem, - kind: event.kind, - data: event.data, - ts: DateTime.now(), - )); - } -} - const List _tier0Namespaces = [ 'builtin.default-layout', 'builtin.welcome', diff --git a/lib/test_app.dart b/lib/test_app.dart index b8ebd101..cd3bbf6e 100644 --- a/lib/test_app.dart +++ b/lib/test_app.dart @@ -14,7 +14,9 @@ library; import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate' show Isolate; +import 'package:flutter/foundation.dart' show compute; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/widgets.dart'; @@ -134,6 +136,44 @@ class _ClideTestAppState extends State { _log('gitEnv', '${tc.gitEnv}'); print('[testmode]'); + + // Boot sequence simulation tests + print('[testmode] --- boot sequence ---'); + + await _testAsync('compute(resolveToolchainPaths)', () async { + final paths = await compute(resolveToolchainPaths, workDir); + return 'git=${paths.git} pql=${paths.pql}'; + }); + + await _testAsync('Isolate.run(resolveToolchainPaths)', () async { + final paths = await Isolate.run(() => resolveToolchainPaths(workDir)); + return 'git=${paths.git} pql=${paths.pql}'; + }); + + await _testAsync('git rev-parse (project.open sim)', () async { + final r = await Process.run(tc.git, ['rev-parse', '--show-toplevel'], + workingDirectory: workDir, environment: tc.gitEnv); + return 'exit=${r.exitCode} ${(r.stdout as String).trim()}'; + }); + + await _testAsync('sequential git calls', () async { + final r1 = await Process.run(tc.git, ['rev-parse', '--show-toplevel'], + workingDirectory: workDir, environment: tc.gitEnv); + final r2 = await Process.run(tc.git, ['rev-parse', '--abbrev-ref', 'HEAD'], + workingDirectory: workDir, environment: tc.gitEnv); + return 'root=${(r1.stdout as String).trim()} branch=${(r2.stdout as String).trim()}'; + }); + + await _testAsync('compute + immediate Process.run', () async { + final paths = await compute(resolveToolchainPaths, workDir); + final tc2 = Toolchain(); + tc2.applyResolved(paths); + final r = await Process.run(tc2.git, ['rev-parse', '--show-toplevel'], + workingDirectory: workDir, environment: tc2.gitEnv); + return 'exit=${r.exitCode} ${(r.stdout as String).trim()}'; + }); + + print('[testmode]'); } // -- ipc category --------------------------------------------------------- @@ -279,6 +319,17 @@ class _ClideTestAppState extends State { setState(() => _results.add(r)); } + Future _testAsync(String label, Future Function() fn) async { + try { + final result = await fn().timeout(const Duration(seconds: 10)); + _addResult(label, true, result); + } on TimeoutException { + _addResult(label, false, 'TIMEOUT (10s)'); + } catch (e) { + _addResult(label, false, '$e'); + } + } + Future _testExec(String label, String bin, List args, String workDir, {Map? env}) async { try { final r = await Process.run(bin, args, workingDirectory: workDir, environment: env)