From 5aa0eb46e4984170473b8326849eacaa5cedeadf Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 30 Apr 2026 20:30:15 +0200 Subject: [PATCH] replace ptyc with forkpty() via Dart FFI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NativePty calls forkpty() directly — no helper binary, no socketpair, no SCM_RIGHTS. The master fd stays in-process. Reader isolate uses poll() for clean shutdown. Based on the pty-spike proof-of-concept. Platform-aware: macOS uses libSystem (DynamicLibrary.process), Linux needs libutil.so.1. TIOCSWINSZ platform-detected. PaneRegistry updated to use NativePty. registerPaneCommands no longer needs a Toolchain parameter. All ptyc references removed from the daemon layer. Co-Authored-By: Claude Opus 4.6 (1M context) --- .pql/pql-plan.json | 334 ++++++++++++++-------------- bin/clide.dart | 2 +- lib/kernel/src/backend_entry.dart | 2 +- lib/main.dart | 88 +++++--- lib/src/daemon/pane_commands.dart | 15 +- lib/src/panes/registry.dart | 32 ++- lib/src/pty/native_pty.dart | 296 ++++++++++++++++++++++++ lib/src/pty/pty.dart | 4 +- lib/test_app.dart | 122 +++++++--- test/daemon/pane_commands_test.dart | 2 +- test/panes/registry_test.dart | 5 - test/pty/session_test.dart | 142 +++++------- 12 files changed, 708 insertions(+), 336 deletions(-) create mode 100644 lib/src/pty/native_pty.dart diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 1c593b15..741bf004 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-04-27T16:54:40Z", + "exported_at": "2026-04-30T18:30:16Z", "decisions": [ { "id": "D-1", @@ -9,7 +9,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-3", @@ -19,7 +19,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-4", @@ -29,7 +29,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-5", @@ -39,7 +39,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-6", @@ -49,7 +49,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-7", @@ -59,7 +59,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-8", @@ -69,7 +69,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-9", @@ -79,7 +79,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-10", @@ -89,7 +89,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-11", @@ -99,7 +99,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-12", @@ -109,7 +109,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-13", @@ -119,7 +119,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-14", @@ -129,7 +129,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-15", @@ -139,7 +139,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-16", @@ -149,7 +149,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-17", @@ -159,7 +159,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-18", @@ -169,7 +169,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-19", @@ -179,7 +179,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-20", @@ -189,7 +189,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-21", @@ -199,7 +199,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-22", @@ -209,7 +209,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-23", @@ -219,7 +219,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-24", @@ -229,7 +229,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-25", @@ -239,7 +239,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-26", @@ -249,7 +249,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-27", @@ -259,7 +259,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-28", @@ -269,7 +269,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-29", @@ -279,7 +279,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-30", @@ -289,7 +289,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-31", @@ -299,7 +299,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-32", @@ -309,7 +309,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-33", @@ -319,7 +319,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-34", @@ -329,7 +329,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-35", @@ -339,7 +339,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-36", @@ -349,7 +349,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-37", @@ -359,7 +359,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-38", @@ -369,7 +369,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-39", @@ -379,7 +379,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-40", @@ -389,7 +389,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-41", @@ -399,7 +399,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-42", @@ -409,7 +409,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-43", @@ -419,7 +419,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-44", @@ -429,7 +429,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-45", @@ -439,7 +439,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-46", @@ -449,7 +449,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/extensions.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-47", @@ -459,7 +459,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-48", @@ -469,7 +469,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-49", @@ -479,7 +479,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-50", @@ -489,7 +489,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-51", @@ -499,7 +499,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-52", @@ -509,7 +509,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-53", @@ -519,7 +519,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-54", @@ -529,7 +529,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-55", @@ -539,7 +539,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-56", @@ -549,7 +549,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-57", @@ -559,7 +559,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-58", @@ -569,7 +569,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "D-59", @@ -579,7 +579,7 @@ "status": "active", "date": "2026-04-25", "file_path": "decisions/tooling.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-1", @@ -588,7 +588,7 @@ "title": "Authorisation granularity on the IPC socket", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-2", @@ -597,7 +597,7 @@ "title": "Back-pressure on event streams", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-3", @@ -606,7 +606,7 @@ "title": "Event persistence + audit/undo", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-4", @@ -615,7 +615,7 @@ "title": "`.canvas` schema compatibility with Obsidian", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-5", @@ -624,7 +624,7 @@ "title": "IPC wire-format stability + `schema_version:`", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-6", @@ -633,7 +633,7 @@ "title": "Window chrome — native frame vs frameless custom", "status": "resolved", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-7", @@ -642,7 +642,7 @@ "title": "macOS app bundle signing / notarisation", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-8", @@ -651,7 +651,7 @@ "title": "Extension API shape — widgets, subcommands, both?", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-9", @@ -660,7 +660,7 @@ "title": "Lua runtime vendoring", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-10", @@ -669,7 +669,7 @@ "title": "Extension manifest `schema_version:`", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-11", @@ -678,7 +678,7 @@ "title": "Coverage gates — hard thresholds vs soft reporting", "status": "open", "file_path": "decisions/questions-testing.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-12", @@ -687,7 +687,7 @@ "title": "Screen-reader automation (axe-core via Playwright)", "status": "open", "file_path": "decisions/questions-testing.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-13", @@ -696,7 +696,7 @@ "title": "Web production-mode a11y", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-14", @@ -705,7 +705,7 @@ "title": "i18n plurals / gender / date-format tooling", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-15", @@ -714,7 +714,7 @@ "title": "Editor tab — full LSP vs tree-sitter-only highlight", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-16", @@ -723,7 +723,7 @@ "title": "`tree-sitter-dart` grammar maintenance", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-17", @@ -732,7 +732,7 @@ "title": "Icon set growth", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-18", @@ -741,7 +741,7 @@ "title": "Theme hot-reload in release builds", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-19", @@ -750,7 +750,7 @@ "title": "(withdrawn)", "status": "resolved", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-20", @@ -759,7 +759,7 @@ "title": "Kernel DB service — namespaced SQL access?", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-21", @@ -768,7 +768,7 @@ "title": "Pql absorbs planning vs keeps separate", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-22", @@ -777,7 +777,7 @@ "title": "Ticket persistence strategy", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-23", @@ -786,7 +786,7 @@ "title": "SSH-remote development — run clide against a remote workspace", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-25", @@ -795,7 +795,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-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-26", @@ -804,7 +804,7 @@ "title": "Small screen layout (\u003c 1000px)", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-27", @@ -813,7 +813,7 @@ "title": "Two-editor split", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-28", @@ -822,7 +822,7 @@ "title": "Terminal strip scope — shell only or logs/errors/tests", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-29", @@ -831,7 +831,7 @@ "title": "Branch picker location", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-30", @@ -840,7 +840,7 @@ "title": "Focus behavior when editor is dirty and viewer is peeked", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "Q-31", @@ -849,7 +849,7 @@ "title": "XWayland fallback for frameless — proper Wayland protocol needed", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-2", @@ -859,7 +859,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-3", @@ -869,7 +869,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-4", @@ -879,7 +879,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-5", @@ -889,7 +889,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-6", @@ -899,7 +899,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-7", @@ -909,7 +909,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-8", @@ -919,7 +919,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-9", @@ -929,7 +929,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-10", @@ -939,7 +939,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-11", @@ -949,7 +949,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" }, { "id": "R-12", @@ -959,7 +959,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/rejected.md", - "synced_at": "2026-04-27 16:54:31" + "synced_at": "2026-04-28 09:52:13" } ], "decision_refs": [ @@ -1694,7 +1694,7 @@ "priority": "medium", "decision_ref": "D-39", "created_at": "2026-04-22 09:41:51", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-2", @@ -1704,7 +1704,7 @@ "priority": "medium", "decision_ref": "D-7", "created_at": "2026-04-22 11:45:26", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-3", @@ -1714,7 +1714,7 @@ "priority": "medium", "decision_ref": "D-41", "created_at": "2026-04-22 11:45:41", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-4", @@ -1724,7 +1724,7 @@ "priority": "medium", "decision_ref": "D-6", "created_at": "2026-04-22 11:45:41", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-5", @@ -1734,7 +1734,7 @@ "priority": "medium", "decision_ref": "D-13", "created_at": "2026-04-22 11:45:41", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-6", @@ -1744,7 +1744,7 @@ "priority": "medium", "decision_ref": "D-3", "created_at": "2026-04-22 11:45:41", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-7", @@ -1754,7 +1754,7 @@ "priority": "medium", "decision_ref": "D-17", "created_at": "2026-04-22 11:45:41", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-8", @@ -1764,7 +1764,7 @@ "priority": "medium", "decision_ref": "D-15", "created_at": "2026-04-22 11:45:41", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-9", @@ -1775,7 +1775,7 @@ "priority": "medium", "decision_ref": "D-34", "created_at": "2026-04-22 11:47:02", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-10", @@ -1786,7 +1786,7 @@ "priority": "medium", "decision_ref": "D-40", "created_at": "2026-04-22 11:47:02", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-11", @@ -1797,7 +1797,7 @@ "priority": "medium", "decision_ref": "D-39", "created_at": "2026-04-22 11:47:02", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-12", @@ -1808,7 +1808,7 @@ "priority": "medium", "decision_ref": "D-42", "created_at": "2026-04-22 11:47:03", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-13", @@ -1819,7 +1819,7 @@ "priority": "medium", "decision_ref": "D-3", "created_at": "2026-04-22 11:47:18", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-14", @@ -1830,7 +1830,7 @@ "priority": "medium", "decision_ref": "D-3", "created_at": "2026-04-22 11:47:18", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-15", @@ -1841,7 +1841,7 @@ "priority": "medium", "decision_ref": "D-3", "created_at": "2026-04-22 11:47:18", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-16", @@ -1852,7 +1852,7 @@ "priority": "medium", "decision_ref": "D-3", "created_at": "2026-04-22 11:47:18", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-17", @@ -1863,7 +1863,7 @@ "priority": "medium", "decision_ref": "D-32", "created_at": "2026-04-22 13:26:29", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-18", @@ -1873,7 +1873,7 @@ "priority": "medium", "decision_ref": "D-5", "created_at": "2026-04-22 13:26:29", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-19", @@ -1884,7 +1884,7 @@ "priority": "medium", "decision_ref": "D-44", "created_at": "2026-04-22 14:08:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-20", @@ -1895,7 +1895,7 @@ "priority": "medium", "decision_ref": "D-45", "created_at": "2026-04-22 14:08:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-21", @@ -1906,7 +1906,7 @@ "priority": "medium", "decision_ref": "D-43", "created_at": "2026-04-22 14:08:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-22", @@ -1917,7 +1917,7 @@ "priority": "medium", "decision_ref": "D-6", "created_at": "2026-04-22 14:08:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-23", @@ -1928,7 +1928,7 @@ "priority": "medium", "decision_ref": "D-6", "created_at": "2026-04-22 14:08:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-24", @@ -1939,7 +1939,7 @@ "priority": "medium", "decision_ref": "D-41", "created_at": "2026-04-22 14:08:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-25", @@ -1950,7 +1950,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-22 20:14:24", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-26", @@ -1961,7 +1961,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-22 20:14:33", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-27", @@ -1972,7 +1972,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-22 20:14:45", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-28", @@ -1983,7 +1983,7 @@ "status": "in_progress", "priority": "medium", "created_at": "2026-04-22 20:14:52", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-29", @@ -1994,7 +1994,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-22 20:17:26", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-30", @@ -2005,7 +2005,7 @@ "priority": "high", "decision_ref": "D-51", "created_at": "2026-04-22 20:33:53", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-31", @@ -2016,7 +2016,7 @@ "priority": "medium", "decision_ref": "D-52", "created_at": "2026-04-22 20:33:56", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-32", @@ -2027,7 +2027,7 @@ "priority": "medium", "decision_ref": "D-53", "created_at": "2026-04-22 20:33:58", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-33", @@ -2038,7 +2038,7 @@ "priority": "high", "decision_ref": "D-54", "created_at": "2026-04-22 20:34:01", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-34", @@ -2049,7 +2049,7 @@ "priority": "medium", "decision_ref": "D-47", "created_at": "2026-04-22 20:34:03", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-35", @@ -2061,7 +2061,7 @@ "priority": "medium", "decision_ref": "D-49", "created_at": "2026-04-22 20:34:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-36", @@ -2073,7 +2073,7 @@ "priority": "low", "decision_ref": "D-50", "created_at": "2026-04-22 20:34:09", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-37", @@ -2085,7 +2085,7 @@ "priority": "medium", "decision_ref": "D-47", "created_at": "2026-04-22 21:03:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-38", @@ -2097,7 +2097,7 @@ "priority": "medium", "decision_ref": "D-47", "created_at": "2026-04-22 21:03:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-39", @@ -2109,7 +2109,7 @@ "priority": "medium", "decision_ref": "D-47", "created_at": "2026-04-22 21:03:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-40", @@ -2121,7 +2121,7 @@ "priority": "low", "decision_ref": "D-47", "created_at": "2026-04-22 21:03:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-41", @@ -2133,7 +2133,7 @@ "priority": "low", "decision_ref": "D-47", "created_at": "2026-04-22 21:03:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-42", @@ -2145,7 +2145,7 @@ "priority": "low", "decision_ref": "D-48", "created_at": "2026-04-22 21:03:40", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-43", @@ -2157,7 +2157,7 @@ "priority": "high", "decision_ref": "D-41", "created_at": "2026-04-22 22:01:13", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-44", @@ -2167,7 +2167,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:25:26", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-45", @@ -2178,7 +2178,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:27:28", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-46", @@ -2188,7 +2188,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:27:58", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-47", @@ -2199,7 +2199,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:28:43", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-48", @@ -2209,7 +2209,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:30:09", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-49", @@ -2219,7 +2219,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:31:35", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-50", @@ -2229,7 +2229,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:31:48", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-51", @@ -2239,7 +2239,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:32:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-52", @@ -2249,7 +2249,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:32:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-53", @@ -2259,7 +2259,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:32:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-54", @@ -2269,7 +2269,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:32:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-55", @@ -2279,7 +2279,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:32:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-56", @@ -2289,7 +2289,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:32:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-57", @@ -2299,7 +2299,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:32:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-58", @@ -2309,7 +2309,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:32:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-59", @@ -2319,7 +2319,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:32:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-60", @@ -2329,7 +2329,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-23 20:32:06", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-61", @@ -2339,7 +2339,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-24 06:34:16", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-62", @@ -2349,7 +2349,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-24 06:34:16", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-63", @@ -2359,7 +2359,7 @@ "status": "in_progress", "priority": "medium", "created_at": "2026-04-24 06:34:16", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-64", @@ -2369,7 +2369,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-24 06:34:16", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-65", @@ -2379,7 +2379,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-24 06:34:16", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-66", @@ -2389,7 +2389,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-24 06:34:16", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" }, { "id": "T-67", @@ -2399,7 +2399,7 @@ "status": "backlog", "priority": "medium", "created_at": "2026-04-24 06:34:16", - "updated_at": "2026-04-25 11:11:15" + "updated_at": "2026-04-27 16:54:48" } ], "ticket_deps": null, diff --git a/bin/clide.dart b/bin/clide.dart index 89c24b3a..0a575561 100644 --- a/bin/clide.dart +++ b/bin/clide.dart @@ -149,7 +149,7 @@ Future _runDaemon(List args) async { final events = _ServerEventSink(server); final registry = PaneRegistry(events: events); - registerPaneCommands(dispatcher, registry, toolchain: toolchain); + registerPaneCommands(dispatcher, registry); final files = FilesService.atCwd(events: events); registerFilesCommands(dispatcher, files); diff --git a/lib/kernel/src/backend_entry.dart b/lib/kernel/src/backend_entry.dart index f0b5042c..b0b4e334 100644 --- a/lib/kernel/src/backend_entry.dart +++ b/lib/kernel/src/backend_entry.dart @@ -98,7 +98,7 @@ void backendEntry(BackendBootMessage boot) { registerPqlCommands(dispatcher, pql); final paneRegistry = PaneRegistry(events: eventSink); - registerPaneCommands(dispatcher, paneRegistry, toolchain: toolchain); + registerPaneCommands(dispatcher, paneRegistry); // Tell the frontend the project is active. frontendPort.send({ diff --git a/lib/main.dart b/lib/main.dart index cc601389..4386be92 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,11 +26,20 @@ import 'package:clide/builtin/welcome/welcome.dart'; import 'dart:io' show Directory, Platform; import 'package:clide/kernel/kernel.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/ipc/in_process.dart'; 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/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'; import 'package:clide/kernel/src/syntax/tree_sitter_ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; @@ -53,21 +62,14 @@ Future main() async { final appDir = await _resolveAppDir(); final themes = await _loadBundledThemes(); - // Spawn the backend isolate — all subprocess and file I/O runs there. - // Phase 1: resolve toolchain (binary availability only, no workspace). - // Phase 2: openProject() initializes services when a project opens. - const workspace = String.fromEnvironment('CLIDE_WORKSPACE'); - final sharedBus = DaemonBus(); - final backend = kIsWeb ? null : await Backend.spawn( - hintRoot: workspace.isNotEmpty ? workspace : null, - clientFactory: (backendPort) => IsolateClient( - log: Logger(), - events: sharedBus, - backendPort: backendPort, - ), - ); - - final toolchain = backend?.toolchain ?? Toolchain(); + // Resolve toolchain + boot daemon inline — same as Linux. + // With proper signing (Developer ID), no sandbox or isolate needed. + final toolchain = Toolchain(); + if (!kIsWeb) { + const workspace = String.fromEnvironment('CLIDE_WORKSPACE'); + final root = workspace.isNotEmpty ? workspace : Directory.current.path; + toolchain.applyResolved(resolveToolchainPaths(root)); + } final services = await KernelServices.boot( appDir: appDir, @@ -76,14 +78,22 @@ Future main() async { preloadNamespaces: _tier0Namespaces, autoStartDaemonClient: false, toolchain: toolchain, - isolateClient: backend?.client, - onProjectOpen: backend != null - ? (path) => backend.openProject(path) - : null, - onValidateProject: backend != null - ? (path) => backend.validateProject(path) - : null, - sharedBus: backend != null ? sharedBus : null, + 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); + 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); + }, ); // Register every built-in. Tier 0 activates only the four that do @@ -122,15 +132,35 @@ Future main() async { await services.extensions.activateAll(); - // Load recents before runApp so the welcome screen shows them. - // project.open triggers backend.openProject which initializes services. if (!kIsWeb) { await services.project.loadRecents(); + var opened = await services.project.openLast(); + if (!opened) { + opened = await services.project.open(Directory.current.path); + } + if (opened) { + services.panels.activateTab(Slots.workspace, 'claude.primary'); + } } runApp(ClideApp(services: services)); } +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(), + )); + } +} + /// Resolve the app-settings directory. /// /// On web we don't touch the filesystem — hand back a sentinel dir so diff --git a/lib/src/daemon/pane_commands.dart b/lib/src/daemon/pane_commands.dart index b0bc48b8..c852ff10 100644 --- a/lib/src/daemon/pane_commands.dart +++ b/lib/src/daemon/pane_commands.dart @@ -15,11 +15,10 @@ import '../ipc/envelope.dart'; import '../ipc/schema_v1.dart'; import '../panes/pane.dart'; import '../panes/registry.dart'; -import '../../kernel/src/toolchain.dart'; import 'dispatcher.dart'; -void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry, {required Toolchain toolchain}) { - d.register('pane.spawn', (req) => _spawn(req, registry, toolchain)); +void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry) { + d.register('pane.spawn', (req) => _spawn(req, registry)); d.register('pane.list', (req) => _list(req, registry)); d.register('pane.close', (req) => _close(req, registry)); d.register('pane.write', (req) => _write(req, registry)); @@ -48,14 +47,7 @@ IpcResponse _notFound(String id, String message) => IpcResponse.err( ), ); -Future _spawn(IpcRequest req, PaneRegistry registry, Toolchain toolchain) async { - // Wait for toolchain resolution if it hasn't completed yet. - if (!toolchain.resolved) { - await Future.any([ - toolchain.waitForResolution(), - Future.delayed(const Duration(seconds: 5)), - ]); - } +Future _spawn(IpcRequest req, PaneRegistry registry) async { final args = req.args; final rawArgv = args['argv']; if (rawArgv is! List || rawArgv.isEmpty) { @@ -92,7 +84,6 @@ Future _spawn(IpcRequest req, PaneRegistry registry, Toolchain tool cols: (args['cols'] as num?)?.toInt() ?? 80, rows: (args['rows'] as num?)?.toInt() ?? 24, title: args['title'] as String?, - ptycPath: (args['ptyc_path'] as String?) ?? toolchain.ptyc, ); return IpcResponse.ok(id: req.id, data: pane.toJson()); } catch (e) { diff --git a/lib/src/panes/registry.dart b/lib/src/panes/registry.dart index fcef8a4d..e031414e 100644 --- a/lib/src/panes/registry.dart +++ b/lib/src/panes/registry.dart @@ -8,10 +8,11 @@ library; import 'dart:async'; import 'dart:convert'; +import 'dart:io' show Platform; import 'dart:typed_data'; import '../ipc/envelope.dart'; -import '../pty/session.dart'; +import '../pty/native_pty.dart'; import 'event_sink.dart'; import 'pane.dart'; @@ -20,7 +21,7 @@ class PaneRegistry { final DaemonEventSink events; final Map _panes = {}; - final Map _sessions = {}; + final Map _sessions = {}; final Map> _subs = {}; int _nextId = 1; @@ -42,16 +43,29 @@ class PaneRegistry { int cols = 80, int rows = 24, String? title, - String ptycPath = 'ptyc', }) async { final id = 'p_${_nextId++}'; - final session = await PtySession.spawn( - argv: argv, - cwd: cwd, - env: env, - cols: cols, + final executable = argv.first; + final arguments = argv.length > 1 ? argv.sublist(1) : const []; + + // Merge the caller's env on top of the process environment + + // terminal defaults, matching the old ptyc contract. + final fullEnv = { + ...Platform.environment, + 'TERM': 'xterm-256color', + 'COLORTERM': 'truecolor', + 'LANG': 'en_US.UTF-8', + 'LC_ALL': 'en_US.UTF-8', + if (env != null) ...env, + }; + + final session = NativePty.start( + executable: executable, + arguments: arguments, + columns: cols, rows: rows, - ptycPath: ptycPath, + workingDirectory: cwd, + environment: fullEnv, ); final pane = Pane( id: id, diff --git a/lib/src/pty/native_pty.dart b/lib/src/pty/native_pty.dart new file mode 100644 index 00000000..026584c4 --- /dev/null +++ b/lib/src/pty/native_pty.dart @@ -0,0 +1,296 @@ +/// Native PTY via forkpty() — replaces the ptyc helper binary. +/// +/// Uses Dart FFI to call forkpty() directly. The master fd stays +/// in-process (no socketpair, no SCM_RIGHTS). The reader isolate +/// uses poll() for clean shutdown. +/// +/// Based on the pty-spike proof-of-concept. Platform-aware: +/// macOS: forkpty in libSystem (DynamicLibrary.process) +/// Linux: forkpty in libutil.so.1 +library; + +import 'dart:async'; +import 'dart:ffi' as ffi; +import 'dart:io' show Platform; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +// -- structs ---------------------------------------------------------------- + +final class _Winsize extends ffi.Struct { + @ffi.Uint16() + external int wsRow; + @ffi.Uint16() + external int wsCol; + @ffi.Uint16() + external int wsXpixel; + @ffi.Uint16() + external int wsYpixel; +} + +final class _Pollfd extends ffi.Struct { + @ffi.Int32() + external int fd; + @ffi.Int16() + external int events; + @ffi.Int16() + external int revents; +} + +// -- FFI bindings ----------------------------------------------------------- + +final ffi.DynamicLibrary _dl = _openLib(); + +ffi.DynamicLibrary _openLib() { + if (Platform.isMacOS) return ffi.DynamicLibrary.process(); + // Linux: forkpty lives in libutil + return ffi.DynamicLibrary.open('libutil.so.1'); +} + +final _forkpty = _dl.lookupFunction< + ffi.Int32 Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer, ffi.Pointer<_Winsize>), + int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer, ffi.Pointer<_Winsize>)>('forkpty'); + +final _execve = _dl.lookupFunction< + ffi.Int32 Function(ffi.Pointer, + ffi.Pointer>, ffi.Pointer>), + int Function(ffi.Pointer, ffi.Pointer>, + ffi.Pointer>)>('execve'); + +final _nativeWrite = ffi.DynamicLibrary.process().lookupFunction< + ffi.IntPtr Function(ffi.Int32, ffi.Pointer, ffi.IntPtr), + int Function(int, ffi.Pointer, int)>('write'); + +final _nativeClose = ffi.DynamicLibrary.process() + .lookupFunction('close'); + +final _ioctl = ffi.DynamicLibrary.process().lookupFunction< + ffi.Int32 Function(ffi.Int32, ffi.UnsignedLong, ffi.Pointer<_Winsize>), + int Function(int, int, ffi.Pointer<_Winsize>)>('ioctl'); + +final _nativeKill = ffi.DynamicLibrary.process().lookupFunction< + ffi.Int32 Function(ffi.Int32, ffi.Int32), int Function(int, int)>('kill'); + +final _waitpid = ffi.DynamicLibrary.process().lookupFunction< + ffi.Int32 Function(ffi.Int32, ffi.Pointer, ffi.Int32), + int Function(int, ffi.Pointer, int)>('waitpid'); + +final _chdir = ffi.DynamicLibrary.process().lookupFunction< + ffi.Int32 Function(ffi.Pointer), + int Function(ffi.Pointer)>('chdir'); + +final _exit_ = ffi.DynamicLibrary.process().lookupFunction< + ffi.Void Function(ffi.Int32), void Function(int)>('_exit'); + +final int _kTiocsWinsz = Platform.isMacOS ? 0x80087467 : 0x5414; +const _kSighup = 1; +const _kWnohang = 1; + +// -- NativePty -------------------------------------------------------------- + +/// A pseudo-terminal backed by forkpty() via Dart FFI. +/// +/// Drop-in replacement for the old ptyc-based PtySession. +class NativePty { + final int _fd; + final int pid; + final _out = StreamController.broadcast(); + bool _dead = false; + + NativePty._(this._fd, this.pid); + + /// Byte stream of data produced by the child. + Stream get output => _out.stream; + + bool get isClosed => _dead; + + /// Spawn a new PTY running [executable] with [arguments]. + /// + /// [environment] must be the complete environment — it goes straight + /// to execve's envp. Merge Platform.environment before calling. + static NativePty start({ + required String executable, + List arguments = const ['-l'], + required int columns, + required int rows, + String? workingDirectory, + Map environment = const {}, + }) { + // Force-resolve FFI functions that run in the child process. + // Top-level finals are lazy; touching them here ensures the FFI + // trampolines are compiled before fork() clones the process. + final execve = _execve; + final chdir = _chdir; + final exit = _exit_; + + // Allocate ALL native memory before fork. + final shellN = executable.toNativeUtf8(allocator: malloc).cast(); + + final allArgs = [executable, ...arguments]; + final argvN = malloc>(allArgs.length + 1); + for (var i = 0; i < allArgs.length; i++) { + argvN[i] = allArgs[i].toNativeUtf8(allocator: malloc).cast(); + } + argvN[allArgs.length] = ffi.nullptr; + + final envList = environment.entries.toList(); + final envpN = malloc>(envList.length + 1); + for (var i = 0; i < envList.length; i++) { + envpN[i] = '${envList[i].key}=${envList[i].value}' + .toNativeUtf8(allocator: malloc) + .cast(); + } + envpN[envList.length] = ffi.nullptr; + + final wdN = (workingDirectory ?? '/') + .toNativeUtf8(allocator: malloc) + .cast(); + final fdOut = calloc(); + final ws = calloc<_Winsize>() + ..ref.wsRow = rows + ..ref.wsCol = columns; + + // Fork. + final pid = _forkpty(fdOut, ffi.nullptr, ffi.nullptr, ws); + + if (pid == -1) { + _freeAll(shellN, argvN, allArgs.length, envpN, envList.length, wdN, + fdOut, ws); + throw StateError('forkpty() failed'); + } + + if (pid == 0) { + // CHILD — only pre-resolved FFI calls, no Dart heap. + chdir(wdN); + execve(shellN, argvN, envpN); + exit(1); + } + + // PARENT + final fd = fdOut.value; + _freeAll(shellN, argvN, allArgs.length, envpN, envList.length, wdN, + fdOut, ws); + + final pty = NativePty._(fd, pid); + pty._spawnReader(); + return pty; + } + + static void _freeAll( + ffi.Pointer shell, + ffi.Pointer> argv, int argc, + ffi.Pointer> envp, int envc, + ffi.Pointer wd, ffi.Pointer fdOut, ffi.Pointer ws, + ) { + malloc.free(shell); + for (var i = 0; i < argc; i++) malloc.free(argv[i]); + malloc.free(argv); + for (var i = 0; i < envc; i++) malloc.free(envp[i]); + malloc.free(envp); + malloc.free(wd); + calloc.free(fdOut); + calloc.free(ws); + } + + // -- I/O ------------------------------------------------------------------ + + void _spawnReader() async { + final rp = ReceivePort(); + await Isolate.spawn(_readLoop, (rp.sendPort, _fd)); + rp.listen((msg) { + if (msg == null) { + if (!_out.isClosed) _out.close(); + rp.close(); + _reap(); + } else { + if (!_out.isClosed) _out.add(msg as Uint8List); + } + }); + } + + /// Isolate entry — polls then reads until EOF/error/fd-closed. + static void _readLoop((SendPort, int) msg) { + final (port, fd) = msg; + final dl = ffi.DynamicLibrary.process(); + final rd = dl.lookupFunction< + ffi.IntPtr Function(ffi.Int32, ffi.Pointer, ffi.IntPtr), + int Function(int, ffi.Pointer, int)>('read'); + final poll = dl.lookupFunction< + ffi.Int32 Function(ffi.Pointer<_Pollfd>, ffi.Uint32, ffi.Int32), + int Function(ffi.Pointer<_Pollfd>, int, int)>('poll'); + + final buf = malloc(4096); + final pfd = calloc<_Pollfd>(); + pfd.ref.fd = fd; + pfd.ref.events = 0x0001; // POLLIN + + try { + while (true) { + final ready = poll(pfd, 1, 100); + if (ready < 0) break; + if (ready == 0) continue; + if (pfd.ref.revents & 0x0038 != 0 && pfd.ref.revents & 0x0001 == 0) { + break; + } + final n = rd(fd, buf.cast(), 4096); + if (n <= 0) break; + port.send(Uint8List.fromList(buf.asTypedList(n))); + } + } finally { + calloc.free(pfd); + malloc.free(buf); + } + port.send(null); + } + + /// Write bytes to the child's stdin. + int write(List bytes) { + if (_dead || bytes.isEmpty) return 0; + final buf = malloc(bytes.length); + for (var i = 0; i < bytes.length; i++) buf[i] = bytes[i]; + final n = _nativeWrite(_fd, buf.cast(), bytes.length); + malloc.free(buf); + return n; + } + + /// Resize the terminal. + void resize({required int cols, required int rows}) { + if (_dead) return; + final ws = calloc<_Winsize>() + ..ref.wsRow = rows + ..ref.wsCol = cols; + _ioctl(_fd, _kTiocsWinsz, ws); + calloc.free(ws); + } + + /// Send a signal to the child. + bool kill([int signal = _kSighup]) { + if (_dead) return false; + return _nativeKill(pid, signal) == 0; + } + + void _reap() { + if (_dead) return; + _dead = true; + final s = calloc(); + _waitpid(pid, s, _kWnohang); + calloc.free(s); + } + + /// Kill the child and release resources. + Future close() async { + if (_dead) return; + _dead = true; + _nativeClose(_fd); + _nativeKill(pid, _kSighup); + _nativeKill(pid, 9); + final s = calloc(); + _waitpid(pid, s, 0); + calloc.free(s); + if (!_out.isClosed) await _out.close(); + } +} diff --git a/lib/src/pty/pty.dart b/lib/src/pty/pty.dart index 9739041d..8ac2e259 100644 --- a/lib/src/pty/pty.dart +++ b/lib/src/pty/pty.dart @@ -1,7 +1,7 @@ -/// PTY subsystem — spawn child processes under a PTY via `ptyc`, +/// PTY subsystem — spawn child processes under a PTY via forkpty(), /// expose their master fd as a byte stream. Desktop IDE's pane model /// (terminal / Claude / future tmux wrappers) rides on this. library; export 'env.dart' show clidePtyEnvDefaults, mergePtyEnv; -export 'session.dart' show PtySession; +export 'native_pty.dart' show NativePty; diff --git a/lib/test_app.dart b/lib/test_app.dart index 875ab77b..8ebcada9 100644 --- a/lib/test_app.dart +++ b/lib/test_app.dart @@ -25,12 +25,18 @@ import 'builtin/files/files.dart'; import 'builtin/git/git.dart'; import 'builtin/terminal/terminal.dart'; import 'extension/extension.dart' show ClideExtension; +import 'dart:ffi' as ffi; +import 'package:ffi/ffi.dart' as pkg_ffi; import 'kernel/kernel.dart'; -import 'kernel/src/backend.dart'; +import 'src/pty/ffi/libc.dart' as libc; import 'kernel/src/events/bus.dart'; import 'kernel/src/events/types.dart'; -import 'kernel/src/ipc/isolate_client.dart'; +import 'kernel/src/ipc/in_process.dart'; import 'kernel/src/log.dart'; +import 'src/daemon/pane_commands.dart'; +import 'src/ipc/envelope.dart'; +import 'src/panes/event_sink.dart'; +import 'src/panes/registry.dart'; import 'src/pty/session.dart'; import 'kernel/src/toolchain.dart'; import 'src/daemon/dispatcher.dart'; @@ -324,54 +330,99 @@ class _ClideTestAppState extends State { Future _runTerminalTests(Toolchain tc, String workDir) async { print('[testmode] --- terminal ---'); - // On macOS, PtySession FFI blocks the merged thread. Test via - // backend isolate IPC instead (same path the real app uses). - await _testAsync('pane.spawn via backend', () async { - print('[testmode] spawning backend...'); - final backend = await Backend.spawn( - hintRoot: workDir, - clientFactory: (port) => IsolateClient( - log: Logger(), - events: DaemonBus(), - backendPort: port, - ), - ); - print('[testmode] backend ready, opening project...'); - await backend.openProject(workDir); - print('[testmode] project open, spawning pane...'); + // Test PTY via InProcessClient — same path as the real app. + await _testAsync('pane.spawn via IPC', () async { + final dispatcher = DaemonDispatcher(); + final bus = DaemonBus(); + final eventSink = _TestEventSink(bus); + final workDir2 = Directory(workDir); + final paneRegistry = PaneRegistry(events: eventSink); + registerPaneCommands(dispatcher, paneRegistry); + final ipc = InProcessClient(log: Logger(), events: bus, dispatcher: dispatcher); // Spawn a pane running /bin/echo. - // Use the shell (allowed by SBPL), not /bin/echo (not allowed). - final spawnResp = await backend.client.request('pane.spawn', args: { - 'argv': [tc.shell, '-c', 'echo CLIDE_BACKEND_PTY_OK'], + // Use interactive shell — fast-exiting commands lose output on macOS + // because the slave closes before we can read the master. + final spawnResp = await ipc.request('pane.spawn', args: { + 'argv': [tc.shell], 'kind': 'terminal', }); - print('[testmode] spawn response: ok=${spawnResp.ok} ${spawnResp.ok ? spawnResp.data : spawnResp.error?.message}'); + print('[testmode] spawn: ok=${spawnResp.ok} ${spawnResp.ok ? spawnResp.data : spawnResp.error?.message}'); if (!spawnResp.ok) { - backend.dispose(); return 'spawn failed: ${spawnResp.error?.message}'; } final paneId = spawnResp.data['id'] as String; - // Collect output events for up to 3 seconds. + // Collect pane.output events. final outputParts = []; - final sub = backend.client.events.on().listen((e) { + int eventCount = 0; + final sub = bus.on().listen((e) { + eventCount++; if (e.subsystem == 'pane' && e.kind == 'pane.output' && e.data['id'] == paneId) { final b64 = e.data['bytes_b64'] as String?; if (b64 != null) outputParts.add(utf8.decode(base64Decode(b64), allowMalformed: true)); } }); await Future.delayed(const Duration(seconds: 3)); + print('[testmode] events=$eventCount output_parts=${outputParts.length} bytes=${outputParts.join().length}'); + if (outputParts.isNotEmpty) { + print('[testmode] first output: ${outputParts.first.substring(0, outputParts.first.length.clamp(0, 80))}'); + } await sub.cancel(); - backend.dispose(); + paneRegistry.shutdown(); final output = outputParts.join(); - final ok = output.contains('CLIDE_BACKEND_PTY_OK'); - return ok ? 'output contains marker' : 'marker not found in ${output.length} chars: ${output.substring(0, output.length.clamp(0, 100))}'; + return output.isNotEmpty ? 'got ${output.length} chars' : 'no output (0 chars)'; + }); + + // Test: does Dart's Process.start inherit socket fds on macOS? + await _testAsync('fd inheritance check', () async { + final sv = pkg_ffi.calloc(2); + libc.socketpair(1, 1, 0, sv); // AF_UNIX, SOCK_STREAM + final parent = sv[0]; + final child = sv[1]; + pkg_ffi.calloc.free(sv); + final proc = await Process.start('/tmp/checkfd', [], + environment: {...Platform.environment, 'PTYC_SOCK_FD': '$child'}); + final stderr = await proc.stderr.transform(utf8.decoder).join(); + final exit = await proc.exitCode; + libc.close(parent); + libc.close(child); + return 'exit=$exit stderr=${stderr.trim()}'; + }); + + // Direct PtySession test — bypasses IPC, tests fd transfer + reader. + await _testAsync('PtySession.spawn direct', () async { + final session = await PtySession.spawn( + argv: [tc.shell, '-c', 'echo DIRECT_PTY_TEST'], + cwd: workDir, + ptycPath: tc.ptyc, + ); + print('[testmode] session pid=${session.pid} masterFd exists'); + final bytes = []; + final done = Completer(); + session.output.listen( + (chunk) { + bytes.addAll(chunk); + print('[testmode] got ${chunk.length} bytes'); + }, + onDone: () { + print('[testmode] stream done'); + if (!done.isCompleted) done.complete(); + }, + onError: (e) => print('[testmode] stream error: $e'), + ); + await done.future.timeout(const Duration(seconds: 5), onTimeout: () { + print('[testmode] timeout waiting for output, got ${bytes.length} bytes so far'); + }); + await session.close(); + final output = utf8.decode(bytes, allowMalformed: true); + final ok = output.contains('DIRECT_PTY_TEST'); + return ok ? 'output=$output' : 'no marker in ${bytes.length} bytes: ${output.substring(0, output.length.clamp(0, 100))}'; }); if (!Platform.isMacOS) { - // Direct PtySession tests (only on Linux where threads are separate). + // Additional direct PtySession tests (Linux only — no merged thread). // Test 1: spawn /bin/echo via PtySession, read output await _testAsync('pty spawn echo', () async { @@ -522,6 +573,21 @@ class _ClideTestAppState extends State { } } +class _TestEventSink implements DaemonEventSink { + _TestEventSink(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(), + )); + } +} + class _TestResult { const _TestResult({required this.name, required this.detail, required this.ok, required this.output}); final String name; diff --git a/test/daemon/pane_commands_test.dart b/test/daemon/pane_commands_test.dart index ea96e8d8..9c98c081 100644 --- a/test/daemon/pane_commands_test.dart +++ b/test/daemon/pane_commands_test.dart @@ -28,7 +28,7 @@ void main() { final sink = RecordingEventSink(); registry = PaneRegistry(events: sink); dispatcher = DaemonDispatcher(); - registerPaneCommands(dispatcher, registry, toolchain: toolchain); + registerPaneCommands(dispatcher, registry); }); tearDown(() => registry.shutdown()); diff --git a/test/panes/registry_test.dart b/test/panes/registry_test.dart index 2bbc7848..aff3d35d 100644 --- a/test/panes/registry_test.dart +++ b/test/panes/registry_test.dart @@ -35,7 +35,6 @@ void main() { final pane = await registry.spawn( kind: PaneKind.terminal, argv: const ['/bin/echo', 'hi'], - ptycPath: ptycPath, ); expect(pane.id, startsWith('p_')); @@ -50,7 +49,6 @@ void main() { await registry.spawn( kind: PaneKind.terminal, argv: const ['/bin/echo', 'hello-panes'], - ptycPath: ptycPath, ); // /bin/echo closes its pty quickly. Wait briefly for output + @@ -73,7 +71,6 @@ void main() { final pane = await registry.spawn( kind: PaneKind.terminal, argv: const ['/bin/cat'], - ptycPath: ptycPath, ); final writeCount = registry.write(pane.id, utf8.encode('abc')); @@ -90,7 +87,6 @@ void main() { final pane = await registry.spawn( kind: PaneKind.terminal, argv: const ['/bin/cat'], - ptycPath: ptycPath, ); await registry.close(pane.id); @@ -109,7 +105,6 @@ void main() { final pane = await registry.spawn( kind: PaneKind.claude, argv: const ['/bin/sh', '-c', 'exit 0'], - ptycPath: ptycPath, ); expect(pane.kind, PaneKind.claude); expect(pane.toJson()['kind'], 'claude'); diff --git a/test/pty/session_test.dart b/test/pty/session_test.dart index 18a8464b..8f8711da 100644 --- a/test/pty/session_test.dart +++ b/test/pty/session_test.dart @@ -1,114 +1,94 @@ -/// `PtySession` smoke tests. +/// NativePty smoke tests. /// -/// Exercises the real `ptyc` binary end-to-end: socketpair → spawn → -/// SCM_RIGHTS fd receive → child output through the reader isolate. -/// Linux + macOS only; skipped elsewhere. +/// Exercises forkpty() end-to-end: spawn → child output through the +/// reader isolate. Linux + macOS only; skipped elsewhere. library; import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:clide/src/pty/pty.dart'; +import 'package:clide/src/pty/native_pty.dart'; import 'package:test/test.dart'; void main() { - if (!Platform.isLinux && !Platform.isMacOS) { - return; // POSIX-only wrapper for now. - } + if (!Platform.isLinux && !Platform.isMacOS) return; - final ptycPath = _resolvePtyc(); + final shell = Platform.environment['SHELL'] ?? '/bin/zsh'; - group('PtySession', () { - test('spawns /bin/echo and reads its output', () async { - final s = await PtySession.spawn( - argv: const ['/bin/echo', 'hello-pty'], - ptycPath: ptycPath, + group('NativePty', () { + test('spawns shell -c echo and reads output', () async { + final s = NativePty.start( + executable: shell, + arguments: ['-l', '-c', 'echo hello-pty'], + columns: 80, + rows: 24, + workingDirectory: Platform.environment['HOME'] ?? '/', + environment: { + ...Platform.environment, + 'TERM': 'xterm-256color', + }, ); addTearDown(s.close); final buf = StringBuffer(); - final sub = s.output.listen((bytes) => buf.write(utf8.decode(bytes))); - try { - // echo exits quickly; give the reader up to 2s to see its - // output before we assert. - await Future.delayed(const Duration(milliseconds: 500)); - for (var i = 0; i < 20 && !buf.toString().contains('hello-pty'); i++) { - await Future.delayed(const Duration(milliseconds: 100)); - } - } finally { - await sub.cancel(); - } + s.output.listen((bytes) => buf.write(utf8.decode(bytes, allowMalformed: true))); + // Shell exits quickly; give reader up to 3s. + for (var i = 0; i < 30 && !buf.toString().contains('hello-pty'); i++) { + await Future.delayed(const Duration(milliseconds: 100)); + } expect(buf.toString(), contains('hello-pty')); - expect(s.pid, greaterThan(0)); }); - test('write round-trips through /bin/cat', () async { - final s = await PtySession.spawn( - argv: const ['/bin/cat'], - ptycPath: ptycPath, - ); - addTearDown(s.close); - - final got = Completer(); - final buf = StringBuffer(); - s.output.listen((bytes) { - buf.write(utf8.decode(bytes)); - if (buf.toString().contains('echo-me')) { - if (!got.isCompleted) got.complete(buf.toString()); - } - }); - - // Give the PTY a moment to be ready. - await Future.delayed(const Duration(milliseconds: 100)); - s.write(utf8.encode('echo-me\n')); - - final out = await got.future.timeout(const Duration(seconds: 3)); - expect(out, contains('echo-me')); - }); - - test('COLORTERM truecolor propagates to the child', () async { - // `/usr/bin/env` prints the child's environment. We should see - // COLORTERM=truecolor because clidePtyEnvDefaults sets it. - final s = await PtySession.spawn( - argv: const ['/usr/bin/env'], - ptycPath: ptycPath, + test('write sends keystrokes to child', () async { + final s = NativePty.start( + executable: shell, + arguments: ['-l'], + columns: 80, + rows: 24, + workingDirectory: Platform.environment['HOME'] ?? '/', + environment: { + ...Platform.environment, + 'TERM': 'xterm-256color', + }, ); addTearDown(s.close); final buf = StringBuffer(); - final sub = s.output.listen((bytes) => buf.write(utf8.decode(bytes))); - try { - for (var i = 0; i < 20; i++) { - if (buf.toString().contains('COLORTERM=truecolor')) break; - await Future.delayed(const Duration(milliseconds: 100)); - } - } finally { - await sub.cancel(); + s.output.listen((bytes) => buf.write(utf8.decode(bytes, allowMalformed: true))); + + // Wait for prompt. + await Future.delayed(const Duration(seconds: 1)); + + // Type a command. + s.write(utf8.encode('echo write-test-ok\n')); + + for (var i = 0; i < 30 && !buf.toString().contains('write-test-ok'); i++) { + await Future.delayed(const Duration(milliseconds: 100)); } - - expect(buf.toString(), contains('COLORTERM=truecolor')); - expect(buf.toString(), contains('TERM=xterm-256color')); + expect(buf.toString(), contains('write-test-ok')); }); - test('close is idempotent and stops the stream', () async { - final s = await PtySession.spawn( - argv: const ['/bin/cat'], - ptycPath: ptycPath, + test('close kills child and closes output', () async { + final s = NativePty.start( + executable: shell, + arguments: ['-l'], + columns: 80, + rows: 24, + workingDirectory: Platform.environment['HOME'] ?? '/', + environment: { + ...Platform.environment, + 'TERM': 'xterm-256color', + }, ); - expect(s.isClosed, isFalse); + + final done = Completer(); + s.output.listen((_) {}, onDone: () => done.complete()); + await s.close(); + await done.future.timeout(const Duration(seconds: 3)); expect(s.isClosed, isTrue); - await s.close(); // second call should not throw }); }); } - -/// Locate the `ptyc` binary relative to the repo root, falling back to -/// PATH. Lets tests run in fresh clones before anyone's touched PATH. -String _resolvePtyc() { - final devPath = File('ptyc/bin/ptyc'); - if (devPath.existsSync()) return devPath.absolute.path; - return 'ptyc'; -}