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:
@@ -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);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/// Resolve the `native/<os>-<arch>/` directory name via FFI ABI introspection
|
||||
/// (T-438 web fence, D-100). Desktop-only; the web build uses
|
||||
/// [native_abi_stub.dart], so `dart:ffi` (here, only `Abi`) stays out of the
|
||||
/// wasm graph.
|
||||
library;
|
||||
|
||||
import 'dart:ffi' show Abi;
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
/// The `<os>-<arch>` dir name for the current process (e.g. `linux-x64`,
|
||||
/// `macos-arm64`) — used to find a dev-tree `clide` binary when running
|
||||
/// un-installed. [abi] is an injection seam for tests; production passes none.
|
||||
String currentNativeDirName({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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/// Web stub (T-438 web fence, D-100): no FFI ABI introspection on web. The
|
||||
/// native dir name only matters for locating a dev-tree `clide` binary, which
|
||||
/// doesn't exist on the web target — so a harmless default suffices.
|
||||
library;
|
||||
|
||||
String currentNativeDirName() => 'linux-x64';
|
||||
@@ -130,12 +130,18 @@ String freshSessionId() {
|
||||
/// same id). Expands an FNV-1a stream into 16 bytes.
|
||||
String _deterministicUuid(String seed) {
|
||||
final bytes = <int>[];
|
||||
var h = 0xcbf29ce484222325;
|
||||
// FNV-1a 64-bit offset basis, split into two 32-bit halves so the dart2js
|
||||
// web fallback accepts it (a full 64-bit literal "can't be represented
|
||||
// exactly in JavaScript" — T-438). Correct on the VM/wasm; web never derives
|
||||
// a session id (no claude process there).
|
||||
var h = (0xcbf29ce4 << 32) | 0x84222325;
|
||||
const prime = 0x100000001b3;
|
||||
for (var i = 0; i < 16; i++) {
|
||||
for (final c in utf8.encode('$seed:$i')) {
|
||||
h ^= c;
|
||||
h = (h * prime) & 0xFFFFFFFFFFFFFFFF;
|
||||
// 64-bit modular wrap is implicit on the VM/wasm; the explicit
|
||||
// `& 0xFFFFFFFFFFFFFFFF` was a no-op and a dart2js-incompatible literal.
|
||||
h = h * prime;
|
||||
}
|
||||
bytes.add(h & 0xff);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user