From a08f75ae1ccdd64ddb9c967e6c56ee013a082e2b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 3 Jun 2026 15:47:34 +0200 Subject: [PATCH] make CLI argv args reach typed command handlers (T-232) Parameterized subsystem commands were unreachable from the CLI: the argv translator emits {positional, flags} but the handlers read named top-level keys (args['path'], args['id'], ...), and nothing mapped between them -- so 'clide editor open ' returned 'path is required'. The fix needed no new mechanism: D-74's CommandSchema.normalize already folds the argv shape into named args by a declared positional ordering; these commands just never registered a schema. Adopts it for the navigation/drive surface -- editor.open/activate/read/ save/close, files.read/ls, pane.close/focus/resize/write -- with non-required positional schemas, so the only effect is positional->named mapping plus numeric coercion of line/cols/rows. Handlers unchanged; missing-arg errors unchanged. Edit-mutation verbs, pane.spawn, and git arg verbs are deferred (noted on the ticket). Takes effect on app restart (the dispatcher is built once at boot). Closes T-232 (under T-208 'Give Claude hands'). Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 5 ++++ .pql/changelog/tickets/2026-06.sql | 3 +++ CHANGELOG.md | 4 ++++ lib/src/daemon/editor_commands.dart | 18 ++++++++++---- lib/src/daemon/files_commands.dart | 10 ++++++-- lib/src/daemon/pane_commands.dart | 16 +++++++++---- test/daemon/editor_commands_test.dart | 20 ++++++++++++++++ test/daemon/files_commands_test.dart | 9 +++++++ test/daemon/pane_commands_test.dart | 29 +++++++++++++++++++++++ 9 files changed, 103 insertions(+), 11 deletions(-) diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index f2ca99a0..ce353a17 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -107,3 +107,8 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, 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-212', 'description', 'VS Code-style: the GUI offers to install the `clide` shell command on PATH, for users who run the .app without `make install`. A command + command-palette entry that copies/symlinks the bundled C client to a PATH dir and reports success/failure. Acceptance: invoking it makes `clide` resolve on PATH from a fresh shell.', 'VS Code-style: the GUI offers to install the `clide` shell command on PATH, for users who run the .app without `make install`. A command + command-palette entry that copies/symlinks the bundled C client to a PATH dir and reports success/failure. Acceptance: invoking it makes `clide` resolve on PATH from a fresh shell. Refinement (2026-06-03, from live dogfooding): scope should include PROACTIVE detection on launch, not just a palette command. When the clide IDE starts in a repo, check whether ''clide'' resolves on PATH AND points to the C client (not a stale symlink to the GUI bundle runner) -- we hit exactly this: ~/.local/bin/clide was a May-6 symlink to ~/.local/lib/clide/clide (the Flutter GUI), so a bare ''clide pane list'' launched a second app instead of querying. If missing or stale, prompt/offer to install (copy the bundled C client to a PATH dir, VS Code ''Install code command'' style) and report success. This is what lets a fresh agent actually reach the CLI (D-83 names the hosted session primary, but an external agent benefits too). Detecting ''stale GUI symlink'' specifically: the target should be an ELF/Mach-O executable, not a symlink into the bundle. Alternative path the user raised: instead of/alongside this, make the /ide MCP surface reachable (T-225) -- but CLI is primary per D-68.', NULL, '2026-06-03 13:33:43', '2026-06-03 13:33:43', '2026-06-03 13:33:43', NULL, 'e887b93f1b67cf8d7f1919c6954b2232', 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-232', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 13:35:22', '2026-06-03 13:35:22', '2026-06-03 13:35:22', NULL, 'd5c275180bff8e53fbdab085b1f238e5', 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-232', 'description', 'Major dogfood finding (2026-06-03), verified live: parameterized subsystem commands are NOT reachable from the clide CLI. The C client sends raw argv; parseArgv (lib/src/cli/argv_to_request.dart) turns ''clide editor open X'' into args={positional:[X]} (also flags:{}, passthrough:[]). But the typed handlers read NAMED top-level keys: editor.open reads args[''path''] (editor_commands.dart:64), files.read reads args[''path''], pane.close/editor.activate read args[''id''], etc. Nothing maps positional/flags -> those names, so every arg-taking verb returns ''X is required'' from the CLI. Confirmed: ''clide editor open pubspec.yaml'' and ''--path=pubspec.yaml'' both -> ''path is required''; ''clide files read pubspec.yaml'' -> ''files.read requires a path''. grep shows ONLY the new ui_command.dart reads args[''positional'']. Net: an external agent can OBSERVE (no-arg reads: status, git status, files root, pane list, editor active) but cannot DRIVE anything parameterized -- undercutting CLI-first (D-1) and the D-6 parity premise behind the whole T-208 initiative. Needs a decision on the mapping contract: most ergonomic is a per-command POSITIONAL/flag schema declared where the handler registers (extends the co-registered schema of D-74) so ''clide editor open '' binds positional[0]->path; alternative is lifting --flags into top-level named args. Then either remap in argv_dispatch/parseArgv before dispatch, or have handlers read a normalized accessor. High priority: this is the gating bug for ''Give Claude hands''.', 'Major dogfood finding (2026-06-03), verified live: parameterized subsystem commands are NOT reachable from the clide CLI. The C client sends raw argv; parseArgv (lib/src/cli/argv_to_request.dart) turns ''clide editor open X'' into args={positional:[X]} (also flags:{}, passthrough:[]). But the typed handlers read NAMED top-level keys: editor.open reads args[''path''] (editor_commands.dart:64), files.read reads args[''path''], pane.close/editor.activate read args[''id''], etc. Nothing maps positional/flags -> those names, so every arg-taking verb returns ''X is required'' from the CLI. Confirmed: ''clide editor open pubspec.yaml'' and ''--path=pubspec.yaml'' both -> ''path is required''; ''clide files read pubspec.yaml'' -> ''files.read requires a path''. grep shows ONLY the new ui_command.dart reads args[''positional'']. Net: an external agent can OBSERVE (no-arg reads: status, git status, files root, pane list, editor active) but cannot DRIVE anything parameterized -- undercutting CLI-first (D-1) and the D-6 parity premise behind the whole T-208 initiative. Needs a decision on the mapping contract: most ergonomic is a per-command POSITIONAL/flag schema declared where the handler registers (extends the co-registered schema of D-74) so ''clide editor open '' binds positional[0]->path; alternative is lifting --flags into top-level named args. Then either remap in argv_dispatch/parseArgv before dispatch, or have handlers read a normalized accessor. High priority: this is the gating bug for ''Give Claude hands''. + +Resolved (2026-06-03): NO new mechanism or decision needed -- the contract already existed. D-74''s CommandSchema.normalize (lib/src/ipc/command_schema.dart) already folds the argv shape {positional,flags} into named args using a declared positional ordering, and the dispatcher already runs normalize+validate. The arg-taking commands simply never registered a schema (adoption is opt-in per D-74). Fix = adopt it: added positional schemas (non-required, so missing-arg errors stay as handlers produce them; only effect is positional->named mapping + numeric coercion of line/cols/rows) to editor.open/activate/read/save/close, files.read/ls, pane.close/focus/resize/write. Handlers unchanged. Tests: CLI-shape ({positional:[...]}) dispatch now binds (editor/pane/files command tests). DEFERRED (still named-arg only; in-process UI works, CLI-arg niche): editor.insert/replace-selection/set-selection/set-content (content/selection encoding + active-buffer fallback make positional ambiguous), pane.spawn (argv is a list, not scalar positional), git.* arg verbs (agents use plain git; D-83 external-agent scope). NOTE: takes effect on app RESTART, not hot reload -- the dispatcher is built once at boot.', NULL, '2026-06-03 13:41:38', '2026-06-03 13:41:38', '2026-06-03 13:41:38', NULL, '0655a8efb4c977b404daedd86d926761', 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-232', 'status', 'in_progress', 'done', NULL, '2026-06-03 13:41:42', '2026-06-03 13:41:42', '2026-06-03 13:41:42', NULL, '17995cc58fd4260f4901357531eeafc4', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index d6742120..a421f7ec 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -285,3 +285,6 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority, 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-212', 'task', 'T-209', 'In-app ''Install clide command in PATH'' affordance', 'VS Code-style: the GUI offers to install the `clide` shell command on PATH, for users who run the .app without `make install`. A command + command-palette entry that copies/symlinks the bundled C client to a PATH dir and reports success/failure. Acceptance: invoking it makes `clide` resolve on PATH from a fresh shell. Refinement (2026-06-03, from live dogfooding): scope should include PROACTIVE detection on launch, not just a palette command. When the clide IDE starts in a repo, check whether ''clide'' resolves on PATH AND points to the C client (not a stale symlink to the GUI bundle runner) -- we hit exactly this: ~/.local/bin/clide was a May-6 symlink to ~/.local/lib/clide/clide (the Flutter GUI), so a bare ''clide pane list'' launched a second app instead of querying. If missing or stale, prompt/offer to install (copy the bundled C client to a PATH dir, VS Code ''Install code command'' style) and report success. This is what lets a fresh agent actually reach the CLI (D-83 names the hosted session primary, but an external agent benefits too). Detecting ''stale GUI symlink'' specifically: the target should be an ELF/Mach-O executable, not a symlink into the bundle. Alternative path the user raised: instead of/alongside this, make the /ide MCP surface reachable (T-225) -- but CLI is primary per D-68.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-02 18:13:52', '2026-06-03 13:33:43', NULL, '92777a1e6cc2cb6bbaa9acf783f43275', 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-232', 'task', 'T-208', 'CLI argv args dont reach typed command handlers (positional/flags vs named)', 'Major dogfood finding (2026-06-03), verified live: parameterized subsystem commands are NOT reachable from the clide CLI. The C client sends raw argv; parseArgv (lib/src/cli/argv_to_request.dart) turns ''clide editor open X'' into args={positional:[X]} (also flags:{}, passthrough:[]). But the typed handlers read NAMED top-level keys: editor.open reads args[''path''] (editor_commands.dart:64), files.read reads args[''path''], pane.close/editor.activate read args[''id''], etc. Nothing maps positional/flags -> those names, so every arg-taking verb returns ''X is required'' from the CLI. Confirmed: ''clide editor open pubspec.yaml'' and ''--path=pubspec.yaml'' both -> ''path is required''; ''clide files read pubspec.yaml'' -> ''files.read requires a path''. grep shows ONLY the new ui_command.dart reads args[''positional'']. Net: an external agent can OBSERVE (no-arg reads: status, git status, files root, pane list, editor active) but cannot DRIVE anything parameterized -- undercutting CLI-first (D-1) and the D-6 parity premise behind the whole T-208 initiative. Needs a decision on the mapping contract: most ergonomic is a per-command POSITIONAL/flag schema declared where the handler registers (extends the co-registered schema of D-74) so ''clide editor open '' binds positional[0]->path; alternative is lifting --flags into top-level named args. Then either remap in argv_dispatch/parseArgv before dispatch, or have handlers read a normalized accessor. High priority: this is the gating bug for ''Give Claude hands''. + +Resolved (2026-06-03): NO new mechanism or decision needed -- the contract already existed. D-74''s CommandSchema.normalize (lib/src/ipc/command_schema.dart) already folds the argv shape {positional,flags} into named args using a declared positional ordering, and the dispatcher already runs normalize+validate. The arg-taking commands simply never registered a schema (adoption is opt-in per D-74). Fix = adopt it: added positional schemas (non-required, so missing-arg errors stay as handlers produce them; only effect is positional->named mapping + numeric coercion of line/cols/rows) to editor.open/activate/read/save/close, files.read/ls, pane.close/focus/resize/write. Handlers unchanged. Tests: CLI-shape ({positional:[...]}) dispatch now binds (editor/pane/files command tests). DEFERRED (still named-arg only; in-process UI works, CLI-arg niche): editor.insert/replace-selection/set-selection/set-content (content/selection encoding + active-buffer fallback make positional ambiguous), pane.spawn (argv is a list, not scalar positional), git.* arg verbs (agents use plain git; D-83 external-agent scope). NOTE: takes effect on app RESTART, not hot reload -- the dispatcher is built once at boot.', 'done', 'high', NULL, NULL, 'D-6', '2026-06-03 13:29:49', '2026-06-03 13:41:42', NULL, '45205bdca118ccf9f6dd2c613707efa2', 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 0c4d99df..bd4fe48c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- CLI commands that take arguments now work: `clide editor open `, + `clide files read `, `clide pane focus ` / `resize `, + etc. now bind positional/flag argv to the handler's named args (they + previously returned "X is required" from the CLI). (T-232) - The Claude composer no longer loses a half-typed message when the UI changes under it — e.g. a permission prompt taking the composer's place. The draft (text and caret) is kept per session and restored when the composer returns. diff --git a/lib/src/daemon/editor_commands.dart b/lib/src/daemon/editor_commands.dart index ed9a624b..6810c3b8 100644 --- a/lib/src/daemon/editor_commands.dart +++ b/lib/src/daemon/editor_commands.dart @@ -13,6 +13,7 @@ import 'dart:io' show FileSystemException; import '../editor/buffer.dart' show Selection; import '../editor/registry.dart'; +import '../ipc/command_schema.dart'; import '../ipc/envelope.dart'; import '../ipc/errno_mapping.dart'; import '../ipc/schema_v1.dart'; @@ -20,18 +21,25 @@ import 'dispatcher.dart'; export '../editor/buffer.dart' show Selection; +// Positional schemas so the CLI binds `clide editor ` to the +// named keys the handlers read (T-232, via the D-74 normalize hook). Args are +// declared non-required — the handlers keep their own presence checks — so the +// only effect is positional→named mapping plus numeric coercion of `line`. +const _idArg = CommandSchema(positional: ['id'], args: {'id': ArgSpec()}); + void registerEditorCommands(DaemonDispatcher d, EditorRegistry registry) { - d.register('editor.open', (req) => _open(req, registry)); + d.register('editor.open', (req) => _open(req, registry), + schema: const CommandSchema(positional: ['path', 'line'], args: {'path': ArgSpec(), 'line': ArgSpec(type: ArgType.number)})); d.register('editor.active', (req) => _active(req, registry)); - d.register('editor.activate', (req) => _activate(req, registry)); + d.register('editor.activate', (req) => _activate(req, registry), schema: _idArg); d.register('editor.list', (req) => _list(req, registry)); - d.register('editor.read', (req) => _read(req, registry)); + d.register('editor.read', (req) => _read(req, registry), schema: _idArg); d.register('editor.insert', (req) => _insert(req, registry)); d.register('editor.replace-selection', (req) => _replace(req, registry)); d.register('editor.set-selection', (req) => _setSelection(req, registry)); d.register('editor.set-content', (req) => _setContent(req, registry)); - d.register('editor.save', (req) => _save(req, registry)); - d.register('editor.close', (req) => _close(req, registry)); + d.register('editor.save', (req) => _save(req, registry), schema: _idArg); + d.register('editor.close', (req) => _close(req, registry), schema: _idArg); } IpcResponse _userErr(String id, String msg, {String? hint}) => IpcResponse.err( diff --git a/lib/src/daemon/files_commands.dart b/lib/src/daemon/files_commands.dart index 64718d0c..4c25a1f9 100644 --- a/lib/src/daemon/files_commands.dart +++ b/lib/src/daemon/files_commands.dart @@ -9,11 +9,17 @@ import '../files/listing.dart'; import '../files/path_safety.dart'; import '../files/pql_config.dart'; import '../files/watcher.dart'; +import '../ipc/command_schema.dart'; import '../ipc/envelope.dart'; import '../ipc/schema_v1.dart'; import '../panes/event_sink.dart'; import 'dispatcher.dart'; +/// Positional schema binding `clide files ` to args['path'] +/// (T-232, via D-74 normalize). Non-required — the handlers keep their own +/// path checks — so the only effect is the positional→named mapping. +const _pathArg = CommandSchema(positional: ['path'], args: {'path': ArgSpec()}); + /// Cap on `files.read` response size. UI doesn't render multi-MB /// blobs usefully and a single uncapped call can OOM. Range/stream /// reads will land as a separate command (T-104 follow-up). @@ -112,7 +118,7 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) { } final content = file.readAsStringSync(); return IpcResponse.ok(id: req.id, data: {'path': path, 'content': content}); - }); + }, schema: _pathArg); d.register('files.ls', (req) async { final dir = (req.args['path'] as String?) ?? ''; @@ -135,7 +141,7 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) { 'entries': [for (final e in entries) e.toJson()], }, ); - }); + }, schema: _pathArg); d.register('files.walk', (req) async { final result = await walkFiles(root: files.root, ignore: files.ignore); diff --git a/lib/src/daemon/pane_commands.dart b/lib/src/daemon/pane_commands.dart index 3b00bba9..b6fbf2a4 100644 --- a/lib/src/daemon/pane_commands.dart +++ b/lib/src/daemon/pane_commands.dart @@ -11,6 +11,7 @@ library; import 'dart:convert'; +import '../ipc/command_schema.dart'; import '../ipc/envelope.dart'; import '../ipc/errno_mapping.dart'; import '../ipc/schema_v1.dart'; @@ -27,12 +28,19 @@ import 'dispatcher.dart'; typedef ViewPaneSource = List Function(); void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry, {ViewPaneSource? viewPanes}) { + // Positional schemas bind `clide pane ` to the named keys the + // handlers read (T-232, via D-74 normalize). Non-required — handlers keep + // their presence checks — so the effect is positional→named mapping plus + // numeric coercion of cols/rows. + const idArg = CommandSchema(positional: ['id'], args: {'id': ArgSpec()}); d.register('pane.spawn', (req) => _spawn(req, registry)); d.register('pane.list', (req) => _list(req, registry, viewPanes)); - d.register('pane.close', (req) => _close(req, registry)); - d.register('pane.write', (req) => _write(req, registry)); - d.register('pane.resize', (req) => _resize(req, registry)); - d.register('pane.focus', (req) => _focus(req, registry)); + d.register('pane.close', (req) => _close(req, registry), schema: idArg); + d.register('pane.write', (req) => _write(req, registry), schema: const CommandSchema(positional: ['id', 'text'], args: {'id': ArgSpec(), 'text': ArgSpec()})); + d.register('pane.resize', (req) => _resize(req, registry), + schema: const CommandSchema( + positional: ['id', 'cols', 'rows'], args: {'id': ArgSpec(), 'cols': ArgSpec(type: ArgType.number), 'rows': ArgSpec(type: ArgType.number)})); + d.register('pane.focus', (req) => _focus(req, registry), schema: idArg); d.register('pane.tail', (req) => _tail(req, registry)); } diff --git a/test/daemon/editor_commands_test.dart b/test/daemon/editor_commands_test.dart index baab26b2..09ea8d96 100644 --- a/test/daemon/editor_commands_test.dart +++ b/test/daemon/editor_commands_test.dart @@ -47,6 +47,26 @@ void main() { expect(act['path'], 'doc.md'); }); + test('CLI positional path binds to editor.open (T-232)', () async { + // The C client sends the argv shape {positional:[...]}; the registered + // schema's normalize must map positional[0] -> path so the handler opens + // it (rather than returning "path is required"). + final r = await call('editor.open', { + 'positional': ['doc.md'], + }); + expect(r.ok, isTrue, reason: r.error?.message); + expect(r.data['path'], 'doc.md'); + }); + + test('CLI --line flag coerces to a number and binds (T-232)', () async { + final r = await call('editor.open', { + 'positional': ['doc.md'], + 'flags': {'line': '1'}, + }); + expect(r.ok, isTrue, reason: r.error?.message); + expect(r.data['path'], 'doc.md'); + }); + test('editor.open with a 1-based line jumps the initial selection (T-52)', () async { await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\nthree\n'); final r = await call('editor.open', {'path': 'multi.txt', 'line': 3}); diff --git a/test/daemon/files_commands_test.dart b/test/daemon/files_commands_test.dart index 263714c6..9e71284c 100644 --- a/test/daemon/files_commands_test.dart +++ b/test/daemon/files_commands_test.dart @@ -97,6 +97,15 @@ void main() { expect(r.data['path'], 'README.md'); }); + test('CLI positional path binds to files.read (T-232)', () async { + // argv shape {positional:[...]} → schema normalize → args['path']. + final r = await call('files.read', const { + 'positional': ['README.md'], + }); + expect(r.ok, isTrue, reason: r.error?.message); + expect(r.data['content'], 'hi'); + }); + test('files.read accepts an absolute path under the workspace root', () async { // Regression: the markdown reader publishes absolute skill paths // (e.g. .claude/skills/.../SKILL.md). An absolute path under root diff --git a/test/daemon/pane_commands_test.dart b/test/daemon/pane_commands_test.dart index 5d368f3c..bf4aeed3 100644 --- a/test/daemon/pane_commands_test.dart +++ b/test/daemon/pane_commands_test.dart @@ -193,6 +193,35 @@ void main() { expect(unknown.ok, isFalse); expect(unknown.error!.kind, 'not_found'); }); + + // T-232: the CLI argv shape ({positional, flags}) must reach the handlers + // via the registered schemas' normalize, incl. numeric coercion. + test('CLI positional id binds to pane.focus (T-232)', () async { + final spawn = await call('pane.spawn', { + 'argv': const ['/bin/cat'], + }); + final id = spawn.data['id']! as String; + final r = await call('pane.focus', { + 'positional': [id], + }); + expect(r.ok, isTrue, reason: r.error?.message); + expect(r.data['id'], id); + }); + + test('CLI positional id/cols/rows coerce + bind to pane.resize (T-232)', () async { + final spawn = await call('pane.spawn', { + 'argv': const ['/bin/cat'], + }); + final id = spawn.data['id']! as String; + // cols/rows arrive as strings from argv; the schema coerces them to num + // so the handler (which reads them as num) doesn't see "required". + final r = await call('pane.resize', { + 'positional': [id, '100', '40'], + }); + expect(r.ok, isTrue, reason: r.error?.message); + expect(r.data['cols'], 100); + expect(r.data['rows'], 40); + }); }); // T-219 / D-83: `pane list` reflects the GUI tabs the user sees, merged