diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index dd42cdc3..45581c37 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -1891,3 +1891,4 @@ Design still open: Source: consultants.md "Security — Findings — [Major]" item 1.', NULL, '2026-05-19 13:14:12', '2026-05-19 13:14:12', '2026-05-19 13:14:12', NULL, '3511c5e7731f568de7af33c1970ad97e', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-99', 'status', 'backlog', 'done', NULL, '2026-05-19 13:14:41', '2026-05-19 13:14:41', '2026-05-19 13:14:41', NULL, '5ff47ab0fc7fb8cb8e9e039be48ada2a', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-131', 'status', 'in_progress', 'done', NULL, '2026-05-19 13:14:41', '2026-05-19 13:14:41', '2026-05-19 13:14:41', NULL, 'a57b3fbe4e6815fcbb28691a08130801', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-119', 'status', 'ready', 'in_progress', NULL, '2026-05-19 14:44:43', '2026-05-19 14:44:43', '2026-05-19 14:44:43', NULL, '1d5048cf9a031c504aa3df570a7b0cae', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index 9cdeaa11..7594844d 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -2218,3 +2218,4 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority, 4. D-records updated to match reality. Source: consultants.md "Architecture — Findings — [Critical] No IPC socket server exists".', 'done', 'high', NULL, NULL, NULL, '2026-05-17 18:47:18', '2026-05-19 13:14:41', NULL, '2ad1fbf0b1b6f5b9e46d4a31d1ad830c', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-119', 'task', 'T-99', 'clide panel resize CLI verb (split from T-111)', 'Once T-99 lands an IPC dispatch path, register a ''panel.resize'' command that takes and args and calls LayoutArrangement.setSize / setEditorRatio. Keyboard parity already landed in T-111; this completes user/Claude parity per D-6.', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-18 07:43:50', '2026-05-19 14:44:43', NULL, 'ac4c7a1d76836e546dac66e77ed4cb67', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index 43209a6e..51e74ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added +- `clide panel resize ` CLI verb (T-119) — set an absolute size + with `--to` or nudge with `--by`; `editor` targets the split ratio. + Completes user/Claude parity (D-6) with T-111's keyboard resize. - Unix-domain IPC socket server in the Flutter app (T-99 / T-124). Per-workspace path (D-70: `$XDG_RUNTIME_DIR/clide/.sock` on Linux, `~/Library/Caches/clide/.sock` on macOS). 0600 socket diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7978179d..ecd812b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,6 +97,9 @@ clide tail --events # stream every subsystem event clide tail --events --filter git # stream a single subsystem clide pane focus editor # drive the running app clide panel toggle git # show/hide a panel +clide panel resize sidebar --to 320 # absolute width (px) +clide panel resize context --by -40 # relative nudge (px) +clide panel resize editor --to 0.5 # editor split ratio (0.15–0.70) ``` Output is JSON on stdout, one envelope per line. Streaming verbs diff --git a/integration_test/app_starts_test.dart b/integration_test/app_starts_test.dart index b5db961f..a47370c1 100644 --- a/integration_test/app_starts_test.dart +++ b/integration_test/app_starts_test.dart @@ -49,7 +49,7 @@ void main() { 'builtin.theme-picker', 'builtin.default-layout', ], - daemonClientFactory: (log, events) => FakeDaemonClient(log: log, events: events), + daemonClientFactory: (log, events, _) => FakeDaemonClient(log: log, events: events), autoStartDaemonClient: false, ); services.extensions diff --git a/integration_test/extension_lifecycle_test.dart b/integration_test/extension_lifecycle_test.dart index c2b252de..ea96bcac 100644 --- a/integration_test/extension_lifecycle_test.dart +++ b/integration_test/extension_lifecycle_test.dart @@ -40,7 +40,7 @@ void main() { 'builtin.ipc-status', 'builtin.default-layout', ], - daemonClientFactory: (log, events) => FakeDaemonClient(log: log, events: events), + daemonClientFactory: (log, events, _) => FakeDaemonClient(log: log, events: events), autoStartDaemonClient: false, ); services.extensions diff --git a/integration_test/theme_picker_test.dart b/integration_test/theme_picker_test.dart index 3a5ab9ad..560ec7a7 100644 --- a/integration_test/theme_picker_test.dart +++ b/integration_test/theme_picker_test.dart @@ -31,7 +31,7 @@ void main() { 'builtin.theme-picker', 'builtin.default-layout', ], - daemonClientFactory: (log, events) => FakeDaemonClient(log: log, events: events), + daemonClientFactory: (log, events, _) => FakeDaemonClient(log: log, events: events), autoStartDaemonClient: false, ); services.extensions diff --git a/lib/kernel/src/facade.dart b/lib/kernel/src/facade.dart index 17dd6567..4b3cb582 100644 --- a/lib/kernel/src/facade.dart +++ b/lib/kernel/src/facade.dart @@ -106,7 +106,7 @@ class KernelServices { Locale? initialLocale, List availableLocales = const [Locale('en', 'US')], String? socketPath, - DaemonClient Function(Logger, DaemonBus)? daemonClientFactory, + DaemonClient Function(Logger, DaemonBus, LayoutArrangement)? daemonClientFactory, DaemonClient? isolateClient, bool autoStartDaemonClient = true, Toolchain? toolchain, @@ -164,7 +164,7 @@ class KernelServices { ); final ipc = isolateClient ?? (daemonClientFactory != null - ? daemonClientFactory(log, events) + ? daemonClientFactory(log, events, arrangement) : DaemonClient( // Legacy socket-client fallback — kept until T-127 // replaces it with the in-process socket loopback. diff --git a/lib/main.dart b/lib/main.dart index 6931cf73..7e3ac9e4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,6 +34,8 @@ 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/panel_commands.dart'; +import 'package:clide/src/daemon/panel_resizer_kernel.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'; @@ -81,6 +83,7 @@ Future main() async { DaemonClient? ipcClient; DaemonBus? daemonBus; + LayoutArrangement? kernelArrangement; // IPC socket server (T-99 / T-124, per D-70/71/72). One server per // workspace; restarted when the active project switches because the // socket path is workspace-derived. The local DaemonClient connects @@ -137,7 +140,12 @@ Future main() async { } } - DaemonDispatcher buildDispatcher(DaemonBus events, Toolchain tc, Directory workRoot) { + DaemonDispatcher buildDispatcher( + DaemonBus events, + Toolchain tc, + Directory workRoot, + LayoutArrangement arrangement, + ) { final dispatcher = DaemonDispatcher(); final eventSink = _BusEventSink(events); final paneRegistry = PaneRegistry(events: eventSink); @@ -150,6 +158,7 @@ Future main() async { registerGitCommands(dispatcher, gitClient, eventSink); final pql = PqlClient(workDir: workRoot, toolchain: tc); registerPqlCommands(dispatcher, pql); + registerPanelCommands(dispatcher, ArrangementPanelResizer(arrangement)); registerArgvUnwrap(dispatcher); return dispatcher; } @@ -163,10 +172,11 @@ Future main() async { toolchain: toolchain, daemonClientFactory: kIsWeb ? null - : (log, events) { + : (log, events, arrangement) { daemonBus = events; + kernelArrangement = arrangement; final workRoot = FilesService.atCwd(events: _BusEventSink(events)).root; - final dispatcher = buildDispatcher(events, toolchain, workRoot); + final dispatcher = buildDispatcher(events, toolchain, workRoot, arrangement); // Build the client at the workspace's socket path. The // server is started below (swapIpcServer) which the // client will then auto-connect to via its reconnect @@ -187,8 +197,10 @@ Future main() async { onProjectOpen: kIsWeb ? null : (path) async { - if (daemonBus == null) return; - final dispatcher = buildDispatcher(daemonBus!, toolchain, Directory(path)); + final bus = daemonBus; + final arrangement = kernelArrangement; + if (bus == null || arrangement == null) return; + final dispatcher = buildDispatcher(bus, toolchain, Directory(path), arrangement); await swapIpcServer(dispatcher, Directory(path)); }, ); diff --git a/lib/src/daemon/panel_commands.dart b/lib/src/daemon/panel_commands.dart new file mode 100644 index 00000000..a0b2d28f --- /dev/null +++ b/lib/src/daemon/panel_commands.dart @@ -0,0 +1,162 @@ +/// Registers panel.* command handlers on a [DaemonDispatcher]. +/// +/// Verb list (T-119): +/// panel.resize +/// +/// Mirrors the keyboard-driven resize that landed in T-111 +/// (`lib/kernel/src/panels/drag_resize.dart`): the CLI verb is the +/// other half of D-6's user/Claude parity for panel sizing. +/// +/// Kept Flutter-free so test/daemon/ stays under `dart test` (not +/// `flutter test`). The kernel-side bridge that wraps +/// `LayoutArrangement` lives in +/// `lib/src/daemon/panel_resizer_kernel.dart`. +library; + +import '../ipc/envelope.dart'; +import '../ipc/schema_v1.dart'; +import 'dispatcher.dart'; + +/// Pluggable backend the CLI/IPC layer drives. Kernel-side +/// implementation wraps `LayoutArrangement`; tests substitute an +/// in-memory fake. +abstract class PanelResizer { + /// Apply an absolute pixel size to [slot]. Returns false when + /// the slot is unknown. + bool setSlotSize(String slot, double size); + + /// Bump [slot]'s current size by a raw delta (px). Sign convention + /// matches the drag/keyboard handlers from T-111 — positive deltas + /// grow the slot from its natural edge; right-edge slots flip the + /// sign internally. Returns false when the slot is unknown or has + /// no current size. + bool bumpSlotSize(String slot, double rawDelta); + + /// Set the absolute editor / bottom-panel split ratio. The kernel + /// clamps to its supported range (0.15..0.70 today). + void setEditorRatio(double ratio); + + /// Bump the editor split ratio by [delta] (additive, post-clamp). + void bumpEditorRatio(double delta); + + double? currentSlotSize(String slot); + double get currentEditorRatio; +} + +/// Reserved slot name that routes to [PanelResizer.setEditorRatio] / +/// [PanelResizer.bumpEditorRatio] instead of [PanelResizer.setSlotSize]. +const String editorSplitSlot = 'editor'; + +void registerPanelCommands(DaemonDispatcher d, PanelResizer resizer) { + d.register('panel.resize', (req) => _resize(req, resizer)); +} + +Future _resize(IpcRequest req, PanelResizer r) async { + final view = _ResizeArgs.from(req.args); + if (view.slot == null || view.slot!.isEmpty) { + return _userErr(req.id, 'slot is required (e.g. "sidebar", "context", "$editorSplitSlot")'); + } + if (!view.hasTo && !view.hasBy) { + return _userErr(req.id, 'one of `to` (absolute) or `by` (delta) is required'); + } + if (view.hasTo && view.hasBy) { + return _userErr(req.id, 'pass only one of `to` and `by`'); + } + final value = view.value; + if (value == null) { + return _userErr(req.id, '${view.hasTo ? "to" : "by"} must be numeric'); + } + final slot = view.slot!; + + if (slot == editorSplitSlot) { + if (view.hasTo) { + r.setEditorRatio(value); + } else { + r.bumpEditorRatio(value); + } + return IpcResponse.ok(id: req.id, data: { + 'slot': slot, + 'ratio': r.currentEditorRatio, + }); + } + + final ok = view.hasTo ? r.setSlotSize(slot, value) : r.bumpSlotSize(slot, value); + if (!ok) { + return _notFound(req.id, 'no such slot: $slot'); + } + return IpcResponse.ok(id: req.id, data: { + 'slot': slot, + 'size': r.currentSlotSize(slot), + }); +} + +/// Tiny adapter that lifts `panel.resize` arguments out of either +/// the direct call shape (`{slot: ..., to: ...}`) or the argv- +/// translator shape (`{positional: [slot], flags: {to: '...'}}`). +/// Until T-120 formalises a shared schema, individual commands carry +/// the lift themselves. +class _ResizeArgs { + _ResizeArgs._({ + required this.slot, + required this.hasTo, + required this.hasBy, + required this.value, + }); + + final String? slot; + final bool hasTo; + final bool hasBy; + final double? value; + + factory _ResizeArgs.from(Map args) { + String? slot; + final rawSlot = args['slot']; + if (rawSlot is String) slot = rawSlot; + final positional = args['positional']; + if (slot == null && positional is List && positional.isNotEmpty) { + slot = positional.first.toString(); + } + final flags = args['flags']; + final flagsMap = flags is Map ? flags : const {}; + final hasTo = args.containsKey('to') || flagsMap.containsKey('to'); + final hasBy = args.containsKey('by') || flagsMap.containsKey('by'); + final raw = args.containsKey('to') + ? args['to'] + : args.containsKey('by') + ? args['by'] + : flagsMap.containsKey('to') + ? flagsMap['to'] + : flagsMap['by']; + return _ResizeArgs._( + slot: slot, + hasTo: hasTo, + hasBy: hasBy, + value: _coerceNum(raw), + ); + } + + static double? _coerceNum(Object? v) { + if (v is num) return v.toDouble(); + if (v is String) return double.tryParse(v); + return null; + } +} + +IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err( + id: id, + error: IpcError( + code: IpcExitCode.userError, + kind: IpcErrorKind.userError, + message: message, + hint: hint, + ), + ); + +IpcResponse _notFound(String id, String message) => IpcResponse.err( + id: id, + error: IpcError( + code: IpcExitCode.notFound, + kind: IpcErrorKind.notFound, + message: message, + ), + ); diff --git a/lib/src/daemon/panel_resizer_kernel.dart b/lib/src/daemon/panel_resizer_kernel.dart new file mode 100644 index 00000000..284e0107 --- /dev/null +++ b/lib/src/daemon/panel_resizer_kernel.dart @@ -0,0 +1,49 @@ +/// Kernel-side [PanelResizer] that drives [LayoutArrangement] for +/// the `panel.resize` IPC verb (T-119). +/// +/// Lives in `lib/src/daemon/` next to the other dispatcher wiring, +/// not `lib/kernel/`, because it bridges the Flutter-bound kernel +/// state into the Flutter-free `panel_commands.dart` surface. Same +/// rationale as the `_BusEventSink` in main.dart: the daemon layer +/// owns the adapter, the kernel layer owns the data. +library; + +import 'package:clide/kernel/src/panels/arrangement.dart'; +import 'package:clide/kernel/src/panels/drag_resize.dart' show bumpedSlotSize; +import 'package:clide/kernel/src/panels/slot_id.dart'; +import 'package:clide/src/daemon/panel_commands.dart'; + +class ArrangementPanelResizer implements PanelResizer { + ArrangementPanelResizer(this._a); + + final LayoutArrangement _a; + + @override + bool setSlotSize(String slot, double size) { + final id = SlotId(slot); + if (_a.sizeOf(id) == null) return false; + _a.setSize(id, size); + return true; + } + + @override + bool bumpSlotSize(String slot, double rawDelta) { + final id = SlotId(slot); + final current = _a.sizeOf(id); + if (current == null) return false; + _a.setSize(id, bumpedSlotSize(slot: id, current: current, rawDelta: rawDelta)); + return true; + } + + @override + void setEditorRatio(double ratio) => _a.setEditorRatio(ratio); + + @override + void bumpEditorRatio(double delta) => _a.setEditorRatio(_a.editorRatio + delta); + + @override + double? currentSlotSize(String slot) => _a.sizeOf(SlotId(slot)); + + @override + double get currentEditorRatio => _a.editorRatio; +} diff --git a/test/daemon/panel_commands_test.dart b/test/daemon/panel_commands_test.dart new file mode 100644 index 00000000..e0a8ef6e --- /dev/null +++ b/test/daemon/panel_commands_test.dart @@ -0,0 +1,169 @@ +/// Tests for the `panel.*` command handlers (T-119). +/// +/// Uses an in-memory [PanelResizer] fake so the dispatch surface can +/// be exercised under `dart test` without pulling Flutter into the +/// build (the live kernel resizer wraps `LayoutArrangement`, which is +/// Flutter-bound; tests for that live in +/// `test/kernel/src/panels/arrangement_test.dart`). +library; + +import 'package:clide/clide.dart'; +import 'package:clide/src/daemon/panel_commands.dart'; +import 'package:test/test.dart'; + +void main() { + group('panel.resize dispatch', () { + late DaemonDispatcher dispatcher; + late _FakeResizer resizer; + + setUp(() { + resizer = _FakeResizer( + slots: {'sidebar': 200, 'context': 240, 'workspace': 800}, + editorRatio: 0.35, + ); + dispatcher = DaemonDispatcher(); + registerPanelCommands(dispatcher, resizer); + }); + + Future call(Map args) { + return dispatcher.dispatch(IpcRequest(id: '1', cmd: 'panel.resize', args: args)); + } + + test('rejects a request with no slot', () async { + final r = await call(const {'to': 220}); + expect(r.ok, isFalse); + expect(r.error!.kind, 'user_error'); + expect(r.error!.message, contains('slot')); + }); + + test('rejects a request with neither `to` nor `by`', () async { + final r = await call(const {'slot': 'sidebar'}); + expect(r.ok, isFalse); + expect(r.error!.kind, 'user_error'); + expect(r.error!.message, contains('to')); + expect(r.error!.message, contains('by')); + }); + + test('rejects a request with both `to` and `by`', () async { + final r = await call(const {'slot': 'sidebar', 'to': 200, 'by': 10}); + expect(r.ok, isFalse); + expect(r.error!.kind, 'user_error'); + expect(r.error!.message, contains('only one')); + }); + + test('rejects a non-numeric `to`', () async { + final r = await call(const {'slot': 'sidebar', 'to': 'lots'}); + expect(r.ok, isFalse); + expect(r.error!.kind, 'user_error'); + expect(r.error!.message, contains('numeric')); + }); + + test('rejects an unknown slot with not-found', () async { + final r = await call(const {'slot': 'nonsense', 'to': 100}); + expect(r.ok, isFalse); + expect(r.error!.kind, 'not_found'); + expect(r.error!.message, contains('nonsense')); + }); + + test('absolute `to` sets the slot size and echoes the result', () async { + final r = await call(const {'slot': 'sidebar', 'to': 320}); + expect(r.ok, isTrue, reason: r.error?.message); + expect(r.data['slot'], 'sidebar'); + expect(r.data['size'], 320); + expect(resizer.slots['sidebar'], 320); + }); + + test('relative `by` bumps the slot through PanelResizer.bumpSlotSize', () async { + final r = await call(const {'slot': 'sidebar', 'by': 25}); + expect(r.ok, isTrue, reason: r.error?.message); + expect(resizer.slots['sidebar'], 200 + 25); + expect(resizer.lastBumpSlot, 'sidebar'); + expect(resizer.lastBumpDelta, 25); + }); + + test('editor slot routes to setEditorRatio', () async { + final r = await call(const {'slot': 'editor', 'to': 0.55}); + expect(r.ok, isTrue, reason: r.error?.message); + expect(r.data['ratio'], 0.55); + expect(resizer.editorRatio, 0.55); + }); + + test('editor slot with `by` routes to bumpEditorRatio', () async { + final r = await call(const {'slot': 'editor', 'by': 0.1}); + expect(r.ok, isTrue, reason: r.error?.message); + expect(resizer.editorRatio, closeTo(0.45, 1e-9)); + }); + + group('argv-translator shape', () { + test('positional[0] supplies the slot and flags carry to/by as strings', () async { + final r = await call(const { + 'positional': ['sidebar'], + 'flags': {'to': '275'}, + }); + expect(r.ok, isTrue, reason: r.error?.message); + expect(resizer.slots['sidebar'], 275); + }); + + test('argv shape `by` parses to a delta', () async { + final r = await call(const { + 'positional': ['context'], + 'flags': {'by': '-30'}, + }); + expect(r.ok, isTrue, reason: r.error?.message); + expect(resizer.lastBumpSlot, 'context'); + expect(resizer.lastBumpDelta, -30); + }); + + test('argv shape with neither flag still surfaces a user error', () async { + final r = await call(const { + 'positional': ['sidebar'], + 'flags': {}, + }); + expect(r.ok, isFalse); + expect(r.error!.kind, 'user_error'); + }); + }); + }); +} + +class _FakeResizer implements PanelResizer { + _FakeResizer({required this.slots, required this.editorRatio}); + + final Map slots; + double editorRatio; + + String? lastBumpSlot; + double? lastBumpDelta; + + @override + bool setSlotSize(String slot, double size) { + if (!slots.containsKey(slot)) return false; + slots[slot] = size; + return true; + } + + @override + bool bumpSlotSize(String slot, double rawDelta) { + if (!slots.containsKey(slot)) return false; + lastBumpSlot = slot; + lastBumpDelta = rawDelta; + slots[slot] = slots[slot]! + rawDelta; + return true; + } + + @override + void setEditorRatio(double ratio) { + editorRatio = ratio; + } + + @override + void bumpEditorRatio(double delta) { + editorRatio += delta; + } + + @override + double? currentSlotSize(String slot) => slots[slot]; + + @override + double get currentEditorRatio => editorRatio; +} diff --git a/test/helpers/kernel_fixture.dart b/test/helpers/kernel_fixture.dart index d30b0901..2aecccdd 100644 --- a/test/helpers/kernel_fixture.dart +++ b/test/helpers/kernel_fixture.dart @@ -32,7 +32,7 @@ class KernelFixture { preloadNamespaces: catalogs.keys.toList(), defaultLocale: defaultLocale, initialLocale: initialLocale, - daemonClientFactory: (log, events) { + daemonClientFactory: (log, events, _) { fake = FakeDaemonClient(log: log, events: events); return fake!; }, diff --git a/test/kernel/src/panels/panel_resizer_kernel_test.dart b/test/kernel/src/panels/panel_resizer_kernel_test.dart new file mode 100644 index 00000000..27851611 --- /dev/null +++ b/test/kernel/src/panels/panel_resizer_kernel_test.dart @@ -0,0 +1,58 @@ +/// Tests the kernel-side bridge that connects the Flutter-free +/// `panel.resize` handler (T-119) to the live [LayoutArrangement]. +/// The handler-side tests live in +/// `test/daemon/panel_commands_test.dart` against an in-memory fake. +library; + +import 'package:clide/kernel/kernel.dart'; +import 'package:clide/src/daemon/panel_resizer_kernel.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ArrangementPanelResizer', () { + late LayoutArrangement arrangement; + late ArrangementPanelResizer r; + + setUp(() { + arrangement = LayoutArrangement()..applyPreset(classicPreset()); + r = ArrangementPanelResizer(arrangement); + }); + + test('setSlotSize forwards to LayoutArrangement.setSize and clamps', () { + final ok = r.setSlotSize('sidebar', 10000); + expect(ok, isTrue); + expect(arrangement.sizeOf(Slots.sidebar), arrangement.maxSizeOf(Slots.sidebar)); + expect(r.currentSlotSize('sidebar'), arrangement.sizeOf(Slots.sidebar)); + }); + + test('setSlotSize returns false for an unknown slot', () { + expect(r.setSlotSize('does-not-exist', 250), isFalse); + }); + + test('bumpSlotSize applies the T-111 sign-flip on context panel', () { + final start = arrangement.sizeOf(Slots.contextPanel)!; + final ok = r.bumpSlotSize('context', 40); + expect(ok, isTrue); + // Context sits on the right edge — positive delta shrinks it. + expect(arrangement.sizeOf(Slots.contextPanel), lessThan(start)); + }); + + test('bumpSlotSize returns false for an unknown slot', () { + expect(r.bumpSlotSize('nope', 5), isFalse); + }); + + test('setEditorRatio + currentEditorRatio round-trip through arrangement', () { + r.setEditorRatio(0.5); + expect(arrangement.editorRatio, 0.5); + expect(r.currentEditorRatio, 0.5); + }); + + test('bumpEditorRatio adds to current ratio (kernel re-clamps)', () { + r.setEditorRatio(0.4); + r.bumpEditorRatio(0.2); + expect(arrangement.editorRatio, closeTo(0.60, 1e-9)); + r.bumpEditorRatio(1.0); // out-of-range; kernel clamps to 0.70 + expect(arrangement.editorRatio, 0.70); + }); + }); +}