feat(web): fence dart:ffi behind web stubs so the WASM build compiles (T-438, D-100)

`flutter build web --wasm` had been broken since the tree-sitter/PTY dart:ffi
pivot. Per D-100 (resolving Q-50: keep the web "happy accident" alive), every
native binding now sits behind a `dart.library.ffi` conditional import with a
graceful web stub. Desktop builds are unchanged — no fidelity loss; the web
target degrades (no terminal, native git, or syntax highlighting).

Discriminator is `dart.library.ffi`, not `dart.library.io` — dart2wasm provides
dart:io, so FFI is the only blocker.

Fences:
- PTY: pty_session → pty_backend_io / pty_backend_web (stub throws).
- tree-sitter: pure types → syntax_result.dart; tree_sitter_service is now a
  facade over _ffi/_stub; tree_sitter_boot_io/stub fences TreeSitterLib.init().
- watchdog: watchdog_windows_stub (all -1 sampler).
- claude ABI probe: native_abi_io/stub (was `dart:ffi show Abi`).
- testmode fd-check: fd_check_io/stub.

Also dart2js-safe: the 64-bit FNV literals in session_naming.dart + paths.dart
(the dual JS fallback rejected them) — split into 32-bit halves, dropped a
no-op 64-bit mask. Desktop/wasm hash values unchanged.

CI: added a `web-wasm` job (flutter build web --wasm) so the fence can't rot.
Two FFI-constructing tree-sitter tests import _ffi.dart directly (the analyzer
resolves the conditional facade to the stub branch).

Verified: `flutter build web --wasm` → built; `flutter analyze` clean;
`make test` green. Full Playwright e2e harness wiring is the tracked follow-on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 17:31:24 +02:00
co-authored by Claude Opus 4.8
parent 967db2f8d9
commit ca08c2a17d
28 changed files with 796 additions and 380 deletions
+5 -22
View File
@@ -21,11 +21,14 @@
/// IO wrapper the orchestrator calls. Flutter-free by design.
library;
import 'dart:ffi' show Abi;
import 'dart:io';
import 'package:clide/src/ipc/paths.dart' show workspaceSocketPath;
// Web fence (T-438, D-100): `Abi.current()` (dart:ffi) is desktop-only; the web
// build gets a default dir name with no FFI introspection.
import 'native_abi_stub.dart' if (dart.library.ffi) 'native_abi_io.dart';
/// The `--allowedTools` rule that pre-approves `clide …` Bash calls for a
/// hosted session (T-217), so the agent isn't prompted on every IDE call.
/// Claude Code's settings/flag syntax for a command-scoped Bash rule is
@@ -90,26 +93,6 @@ String? resolveClideCliDir({required String? currentPath, required List<String>
return null;
}
/// The `native/<os>-<arch>/` directory name the Makefile builds the C client
/// into (e.g. `linux-x64`, `macos-arm64`) — used to find the dev-tree binary
/// when clide runs un-installed (dogfooding clide-on-clide).
String nativeClideDirName({Abi? abi}) {
switch (abi ?? Abi.current()) {
case Abi.macosArm64:
return 'macos-arm64';
case Abi.macosX64:
return 'macos-x64';
case Abi.linuxArm64:
return 'linux-arm64';
case Abi.linuxX64:
return 'linux-x64';
default:
// Windows / other — clide is desktop linux/macos today; fall back to a
// best-effort name so the probe simply misses rather than throwing.
return Platform.isMacOS ? 'macos-x64' : 'linux-x64';
}
}
/// The result of [agentBootstrap]: the env delta to overlay and the extra
/// spawn args (context note + allow rule) to prepend to a session's argv.
class AgentBootstrap {
@@ -128,7 +111,7 @@ AgentBootstrap agentBootstrap(String workspaceRoot, {Map<String, String>? base})
final currentPath = (base ?? Platform.environment)['PATH'] ?? Platform.environment['PATH'];
final candidates = <String>[
if (home != null && home.isNotEmpty) '$home/.local/bin',
'$workspaceRoot/native/${nativeClideDirName()}',
'$workspaceRoot/native/${currentNativeDirName()}',
File(Platform.resolvedExecutable).parent.path,
];
final cliDir = resolveClideCliDir(currentPath: currentPath, candidateDirs: candidates, isExecutableFile: _isExecutableFile);