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 <path>' 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) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 15:47:34 +02:00
co-authored by Claude Opus 4.8
parent 4e5d7b1347
commit a08f75ae1c
9 changed files with 103 additions and 11 deletions
@@ -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. 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; 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 <path>'' 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 <path>'' 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;
+3
View File
@@ -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. 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); 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 <path>'' 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);
+4
View File
@@ -88,6 +88,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Fixed ### Fixed
- CLI commands that take arguments now work: `clide editor open <path>`,
`clide files read <path>`, `clide pane focus <id>` / `resize <id> <c> <r>`,
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 - 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 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. (text and caret) is kept per session and restored when the composer returns.
+13 -5
View File
@@ -13,6 +13,7 @@ import 'dart:io' show FileSystemException;
import '../editor/buffer.dart' show Selection; import '../editor/buffer.dart' show Selection;
import '../editor/registry.dart'; import '../editor/registry.dart';
import '../ipc/command_schema.dart';
import '../ipc/envelope.dart'; import '../ipc/envelope.dart';
import '../ipc/errno_mapping.dart'; import '../ipc/errno_mapping.dart';
import '../ipc/schema_v1.dart'; import '../ipc/schema_v1.dart';
@@ -20,18 +21,25 @@ import 'dispatcher.dart';
export '../editor/buffer.dart' show Selection; export '../editor/buffer.dart' show Selection;
// Positional schemas so the CLI binds `clide editor <verb> <args…>` 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) { 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.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.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.insert', (req) => _insert(req, registry));
d.register('editor.replace-selection', (req) => _replace(req, registry)); d.register('editor.replace-selection', (req) => _replace(req, registry));
d.register('editor.set-selection', (req) => _setSelection(req, registry)); d.register('editor.set-selection', (req) => _setSelection(req, registry));
d.register('editor.set-content', (req) => _setContent(req, registry)); d.register('editor.set-content', (req) => _setContent(req, registry));
d.register('editor.save', (req) => _save(req, registry)); d.register('editor.save', (req) => _save(req, registry), schema: _idArg);
d.register('editor.close', (req) => _close(req, registry)); d.register('editor.close', (req) => _close(req, registry), schema: _idArg);
} }
IpcResponse _userErr(String id, String msg, {String? hint}) => IpcResponse.err( IpcResponse _userErr(String id, String msg, {String? hint}) => IpcResponse.err(
+8 -2
View File
@@ -9,11 +9,17 @@ import '../files/listing.dart';
import '../files/path_safety.dart'; import '../files/path_safety.dart';
import '../files/pql_config.dart'; import '../files/pql_config.dart';
import '../files/watcher.dart'; import '../files/watcher.dart';
import '../ipc/command_schema.dart';
import '../ipc/envelope.dart'; import '../ipc/envelope.dart';
import '../ipc/schema_v1.dart'; import '../ipc/schema_v1.dart';
import '../panes/event_sink.dart'; import '../panes/event_sink.dart';
import 'dispatcher.dart'; import 'dispatcher.dart';
/// Positional schema binding `clide files <verb> <path>` 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 /// Cap on `files.read` response size. UI doesn't render multi-MB
/// blobs usefully and a single uncapped call can OOM. Range/stream /// blobs usefully and a single uncapped call can OOM. Range/stream
/// reads will land as a separate command (T-104 follow-up). /// 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(); final content = file.readAsStringSync();
return IpcResponse.ok(id: req.id, data: {'path': path, 'content': content}); return IpcResponse.ok(id: req.id, data: {'path': path, 'content': content});
}); }, schema: _pathArg);
d.register('files.ls', (req) async { d.register('files.ls', (req) async {
final dir = (req.args['path'] as String?) ?? ''; 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()], 'entries': [for (final e in entries) e.toJson()],
}, },
); );
}); }, schema: _pathArg);
d.register('files.walk', (req) async { d.register('files.walk', (req) async {
final result = await walkFiles(root: files.root, ignore: files.ignore); final result = await walkFiles(root: files.root, ignore: files.ignore);
+12 -4
View File
@@ -11,6 +11,7 @@ library;
import 'dart:convert'; import 'dart:convert';
import '../ipc/command_schema.dart';
import '../ipc/envelope.dart'; import '../ipc/envelope.dart';
import '../ipc/errno_mapping.dart'; import '../ipc/errno_mapping.dart';
import '../ipc/schema_v1.dart'; import '../ipc/schema_v1.dart';
@@ -27,12 +28,19 @@ import 'dispatcher.dart';
typedef ViewPaneSource = List<ViewPane> Function(); typedef ViewPaneSource = List<ViewPane> Function();
void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry, {ViewPaneSource? viewPanes}) { void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry, {ViewPaneSource? viewPanes}) {
// Positional schemas bind `clide pane <verb> <args…>` 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.spawn', (req) => _spawn(req, registry));
d.register('pane.list', (req) => _list(req, registry, viewPanes)); d.register('pane.list', (req) => _list(req, registry, viewPanes));
d.register('pane.close', (req) => _close(req, registry)); d.register('pane.close', (req) => _close(req, registry), schema: idArg);
d.register('pane.write', (req) => _write(req, registry)); 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)); d.register('pane.resize', (req) => _resize(req, registry),
d.register('pane.focus', (req) => _focus(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)); d.register('pane.tail', (req) => _tail(req, registry));
} }
+20
View File
@@ -47,6 +47,26 @@ void main() {
expect(act['path'], 'doc.md'); 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 { 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'); await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\nthree\n');
final r = await call('editor.open', {'path': 'multi.txt', 'line': 3}); final r = await call('editor.open', {'path': 'multi.txt', 'line': 3});
+9
View File
@@ -97,6 +97,15 @@ void main() {
expect(r.data['path'], 'README.md'); 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 { test('files.read accepts an absolute path under the workspace root', () async {
// Regression: the markdown reader publishes absolute skill paths // Regression: the markdown reader publishes absolute skill paths
// (e.g. .claude/skills/.../SKILL.md). An absolute path under root // (e.g. .claude/skills/.../SKILL.md). An absolute path under root
+29
View File
@@ -193,6 +193,35 @@ void main() {
expect(unknown.ok, isFalse); expect(unknown.ok, isFalse);
expect(unknown.error!.kind, 'not_found'); 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 // T-219 / D-83: `pane list` reflects the GUI tabs the user sees, merged