chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)

Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 12:11:53 +02:00
co-authored by Claude Opus 4.8
parent bcea5f15b7
commit 6d0ebab721
444 changed files with 7587 additions and 12849 deletions
+15 -36
View File
@@ -77,19 +77,10 @@ class DaemonDispatcher {
return h(IpcRequest(id: req.id, cmd: req.cmd, args: result.values!));
}
Future<IpcResponse> _ping(IpcRequest req) async => IpcResponse.ok(
id: req.id,
data: {
'pong': true,
'ts': DateTime.now().toUtc().toIso8601String(),
'version': clideVersion,
},
);
Future<IpcResponse> _ping(IpcRequest req) async =>
IpcResponse.ok(id: req.id, data: {'pong': true, 'ts': DateTime.now().toUtc().toIso8601String(), 'version': clideVersion});
Future<IpcResponse> _version(IpcRequest req) async => IpcResponse.ok(
id: req.id,
data: {'version': clideVersion},
);
Future<IpcResponse> _version(IpcRequest req) async => IpcResponse.ok(id: req.id, data: {'version': clideVersion});
/// Reflects the live command registry so the surface is discoverable, not
/// just present (T-248). Every registered verb is listed — split into
@@ -139,11 +130,7 @@ class DaemonDispatcher {
tools.add({
'name': '$prefix$cmd',
'description': subsystem.isEmpty ? verb : '$subsystem: $verb',
'inputSchema': {
'type': 'object',
'properties': props,
if (required.isNotEmpty) 'required': required,
},
'inputSchema': {'type': 'object', 'properties': props, if (required.isNotEmpty) 'required': required},
});
}
return tools;
@@ -153,17 +140,9 @@ class DaemonDispatcher {
static Map<String, Object?> _argJsonSchema(ArgSpec s) {
switch (s.type) {
case ArgType.string:
return {
'type': 'string',
if (s.allowed != null) 'enum': (s.allowed!.toList()..sort()),
if (s.pattern != null) 'pattern': s.pattern!.pattern,
};
return {'type': 'string', if (s.allowed != null) 'enum': (s.allowed!.toList()..sort()), if (s.pattern != null) 'pattern': s.pattern!.pattern};
case ArgType.number:
return {
'type': 'number',
if (s.min != null) 'minimum': s.min,
if (s.max != null) 'maximum': s.max,
};
return {'type': 'number', if (s.min != null) 'minimum': s.min, if (s.max != null) 'maximum': s.max};
case ArgType.boolean:
return {'type': 'boolean'};
case ArgType.stringList:
@@ -176,13 +155,13 @@ class DaemonDispatcher {
}
static Map<String, Object?> _argSpecJson(ArgSpec s) => {
'type': s.type.name,
if (s.required) 'required': true,
if (s.allowed != null) 'allowed': (s.allowed!.toList()..sort()),
if (s.pattern != null) 'pattern': s.pattern!.pattern,
if (s.min != null) 'min': s.min,
if (s.max != null) 'max': s.max,
if (s.maxItems != null) 'maxItems': s.maxItems,
if (s.rejectLeadingDash) 'rejectLeadingDash': true,
};
'type': s.type.name,
if (s.required) 'required': true,
if (s.allowed != null) 'allowed': (s.allowed!.toList()..sort()),
if (s.pattern != null) 'pattern': s.pattern!.pattern,
if (s.min != null) 'min': s.min,
if (s.max != null) 'max': s.max,
if (s.maxItems != null) 'maxItems': s.maxItems,
if (s.rejectLeadingDash) 'rejectLeadingDash': true,
};
}
+20 -28
View File
@@ -28,8 +28,17 @@ export '../editor/buffer.dart' show Selection;
const _idArg = CommandSchema(positional: ['id'], args: {'id': ArgSpec()});
void registerEditorCommands(DaemonDispatcher d, EditorRegistry 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.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), schema: _idArg);
d.register('editor.list', (req) => _list(req, registry));
@@ -43,23 +52,14 @@ void registerEditorCommands(DaemonDispatcher d, EditorRegistry registry) {
}
IpcResponse _userErr(String id, String msg, {String? hint}) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: msg,
hint: hint,
),
);
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: msg, hint: hint),
);
IpcResponse _notFound(String id, String msg) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.notFound,
kind: IpcErrorKind.notFound,
message: msg,
),
);
id: id,
error: IpcError(code: IpcExitCode.notFound, kind: IpcErrorKind.notFound, message: msg),
);
String? _resolveId(IpcRequest req, EditorRegistry r) {
final id = req.args['id'] as String?;
@@ -94,20 +94,12 @@ Future<IpcResponse> _open(IpcRequest req, EditorRegistry r) async {
}
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'editor.open failed: ${e.message}',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'editor.open failed: ${e.message}'),
);
} catch (e) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'editor.open failed: $e',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'editor.open failed: $e'),
);
}
}
@@ -145,7 +137,7 @@ Future<IpcResponse> _list(IpcRequest req, EditorRegistry r) async {
return IpcResponse.ok(
id: req.id,
data: {
'buffers': [for (final b in r.buffers) b.toJson()]
'buffers': [for (final b in r.buffers) b.toJson()],
},
);
}
+23 -43
View File
@@ -28,12 +28,7 @@ const int _filesReadMaxBytes = 10 * 1024 * 1024;
/// Daemon-side state for the `files` subsystem. Holds one
/// [FileWatcher] rooted at the workspace and a resolved [IgnoreSet].
class FilesService {
FilesService({
required this.root,
required this.events,
IgnoreSet? ignore,
this.extraReadRoots = const [],
}) : ignore = ignore ?? _defaultIgnore(root);
FilesService({required this.root, required this.events, IgnoreSet? ignore, this.extraReadRoots = const []}) : ignore = ignore ?? _defaultIgnore(root);
/// Build from the current working directory, walking up to the git
/// root if present. Falls back to CWD otherwise.
@@ -58,12 +53,7 @@ class FilesService {
_watcher = w;
await w.start();
w.stream.listen((change) {
events.emit(IpcEvent(
subsystem: 'files',
kind: 'files.changed',
timestamp: DateTime.now().toUtc(),
data: change.toJson(),
));
events.emit(IpcEvent(subsystem: 'files', kind: 'files.changed', timestamp: DateTime.now().toUtc(), data: change.toJson()));
});
}
@@ -74,20 +64,15 @@ class FilesService {
}
void registerFilesCommands(DaemonDispatcher d, FilesService files) {
d.register(
'files.root',
(req) async => IpcResponse.ok(
id: req.id,
data: {
'path': files.root.absolute.path,
'ignorePatterns': files.ignore.length,
},
));
d.register('files.root', (req) async => IpcResponse.ok(id: req.id, data: {'path': files.root.absolute.path, 'ignorePatterns': files.ignore.length}));
d.register('files.read', (req) async {
final path = req.args['path'] as String?;
if (path == null || path.isEmpty) {
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'files.read requires a path'));
return IpcResponse.err(
id: req.id,
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'files.read requires a path'),
);
}
final String absPath;
try {
@@ -96,11 +81,17 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
// the trusted extra read roots (Claude config dirs, D-80).
absPath = resolveUnderRootsFollowingSymlinks(files.root, files.extraReadRoots, path);
} on PathOutsideRoot {
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'path outside workspace: $path'));
return IpcResponse.err(
id: req.id,
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'path outside workspace: $path'),
);
}
final file = File(absPath);
if (!file.existsSync()) {
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'file not found: $path'));
return IpcResponse.err(
id: req.id,
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'file not found: $path'),
);
}
// Cap response size so a single IPC call can't OOM the UI on a
// multi-gigabyte log file. Caller can paginate / stream via a
@@ -109,11 +100,7 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
if (length > _filesReadMaxBytes) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'file too large: $path ($length bytes; cap $_filesReadMaxBytes)',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'file too large: $path ($length bytes; cap $_filesReadMaxBytes)'),
);
}
final content = file.readAsStringSync();
@@ -126,14 +113,13 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
try {
resolveUnderRootFollowingSymlinks(files.root, dir);
} on PathOutsideRoot {
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'path outside workspace: $dir'));
return IpcResponse.err(
id: req.id,
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'path outside workspace: $dir'),
);
}
}
final entries = await listDir(
root: files.root,
dir: dir,
ignore: files.ignore,
);
final entries = await listDir(root: files.root, dir: dir, ignore: files.ignore);
return IpcResponse.ok(
id: req.id,
data: {
@@ -156,10 +142,7 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
d.register('files.watch', (req) async {
await files.startWatching();
return IpcResponse.ok(
id: req.id,
data: const {'subscribed': true},
);
return IpcResponse.ok(id: req.id, data: const {'subscribed': true});
});
}
@@ -192,8 +175,5 @@ IgnoreSet _defaultIgnore(Directory root) {
// Merge: built-in patterns first, user patterns last. "Last match
// wins" semantics give the user the ability to un-ignore via `!`
// in a future extension of the matcher.
return IgnoreSet([
...IgnoreSet.builtin().patterns,
...user.patterns,
]);
return IgnoreSet([...IgnoreSet.builtin().patterns, ...user.patterns]);
}
+33 -76
View File
@@ -28,12 +28,7 @@ const int _gitPathsMaxCount = 256;
/// handler; the leading-dash guard stops argv injection at the
/// dispatcher (mirrors [validateGitRef], kept below as
/// defense-in-depth since the git client is also UI-reachable).
const CommandSchema _checkoutSchema = CommandSchema(
positional: ['branch'],
args: {
'branch': ArgSpec(required: true, rejectLeadingDash: true),
},
);
const CommandSchema _checkoutSchema = CommandSchema(positional: ['branch'], args: {'branch': ArgSpec(required: true, rejectLeadingDash: true)});
/// Schema for `git.push` (D-74). `clide git push <remote> <branch>`.
/// Both refs optional (bare `git.push` is valid); leading-dash
@@ -47,11 +42,7 @@ const CommandSchema _pushSchema = CommandSchema(
},
);
void registerGitCommands(
DaemonDispatcher d,
GitClient git,
DaemonEventSink events,
) {
void registerGitCommands(DaemonDispatcher d, GitClient git, DaemonEventSink events) {
d.register('git.status', (req) async {
try {
final status = await git.status();
@@ -68,10 +59,13 @@ void registerGitCommands(
final tooMany = _tooManyPaths(req.id, paths);
if (tooMany != null) return tooMany;
final diffs = await git.diff(staged: staged, paths: paths);
return IpcResponse.ok(id: req.id, data: {
'staged': staged,
'diffs': [for (final d in diffs) d.toJson()],
});
return IpcResponse.ok(
id: req.id,
data: {
'staged': staged,
'diffs': [for (final d in diffs) d.toJson()],
},
);
} on GitException catch (e) {
return _gitError(req.id, e);
}
@@ -82,12 +76,7 @@ void registerGitCommands(
if (paths.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.stage requires paths',
hint: 'pass {paths: ["file.txt"]}',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.stage requires paths', hint: 'pass {paths: ["file.txt"]}'),
);
}
final tooMany = _tooManyPaths(req.id, paths);
@@ -127,11 +116,7 @@ void registerGitCommands(
if (patch == null || patch.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.stage-hunk requires a patch',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.stage-hunk requires a patch'),
);
}
try {
@@ -148,11 +133,7 @@ void registerGitCommands(
if (patch == null || patch.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.unstage-hunk requires a patch',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.unstage-hunk requires a patch'),
);
}
try {
@@ -169,11 +150,7 @@ void registerGitCommands(
if (paths.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.discard requires paths',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.discard requires paths'),
);
}
try {
@@ -190,11 +167,7 @@ void registerGitCommands(
if (message == null || message.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.commit requires a message',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.commit requires a message'),
);
}
try {
@@ -233,18 +206,17 @@ void registerGitCommands(
if (count > _gitLogMaxCount) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.log count $count exceeds cap $_gitLogMaxCount',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.log count $count exceeds cap $_gitLogMaxCount'),
);
}
try {
final entries = await git.log(count: count);
return IpcResponse.ok(id: req.id, data: {
'entries': [for (final e in entries) e.toJson()],
});
return IpcResponse.ok(
id: req.id,
data: {
'entries': [for (final e in entries) e.toJson()],
},
);
} on GitException catch (e) {
return _gitError(req.id, e);
}
@@ -275,11 +247,14 @@ void registerGitCommands(
d.register('git.branches', (req) async {
try {
final b = await git.branches();
return IpcResponse.ok(id: req.id, data: {
'branches': [
for (final e in b) {'name': e.name, 'current': e.current}
],
});
return IpcResponse.ok(
id: req.id,
data: {
'branches': [
for (final e in b) {'name': e.name, 'current': e.current},
],
},
);
} on GitException catch (e) {
return _gitError(req.id, e);
}
@@ -290,11 +265,7 @@ void registerGitCommands(
if (branch == null || branch.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'git.checkout requires a branch',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'git.checkout requires a branch'),
);
}
try {
@@ -317,31 +288,17 @@ IpcResponse? _tooManyPaths(String id, List<String> paths) {
if (paths.length <= _gitPathsMaxCount) return null;
return IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'paths length ${paths.length} exceeds cap $_gitPathsMaxCount',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'paths length ${paths.length} exceeds cap $_gitPathsMaxCount'),
);
}
void _emitChanged(DaemonEventSink events) {
events.emit(IpcEvent(
subsystem: 'git',
kind: 'git.changed',
timestamp: DateTime.now().toUtc(),
data: const {},
));
events.emit(IpcEvent(subsystem: 'git', kind: 'git.changed', timestamp: DateTime.now().toUtc(), data: const {}));
}
IpcResponse _gitError(String id, GitException e) {
return IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: e.message,
hint: e.stderr.isNotEmpty ? e.stderr : null,
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: e.message, hint: e.stderr.isNotEmpty ? e.stderr : null),
);
}
+7 -19
View File
@@ -35,11 +35,7 @@ typedef ImagePathResolver = String? Function(String path);
/// publisher so both ends point at one name.
const imageShowChannel = 'image';
void registerImageCommands(
DaemonDispatcher d,
MessagePublisher? Function() publisher, {
ImagePathResolver? resolve,
}) {
void registerImageCommands(DaemonDispatcher d, MessagePublisher? Function() publisher, {ImagePathResolver? resolve}) {
d.register(
'image.show',
(req) async => _show(req, publisher, resolve),
@@ -55,15 +51,11 @@ void registerImageCommands(
}
IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err(
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
);
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
);
Future<IpcResponse> _show(
IpcRequest req,
MessagePublisher? Function() publisherSource,
ImagePathResolver? resolve,
) async {
Future<IpcResponse> _show(IpcRequest req, MessagePublisher? Function() publisherSource, ImagePathResolver? resolve) async {
final path = req.args['path'] as String?;
if (path == null || path.trim().isEmpty) {
return _userErr(req.id, 'an image path is required (e.g. `image show docs/diagram.png`)');
@@ -71,11 +63,7 @@ Future<IpcResponse> _show(
final ext = _extensionOf(path);
if (!imageShowExtensions.contains(ext)) {
return _userErr(
req.id,
'unsupported image format${ext.isEmpty ? '' : ' ".$ext"'}',
hint: 'one of: ${(imageShowExtensions.toList()..sort()).join(', ')}',
);
return _userErr(req.id, 'unsupported image format${ext.isEmpty ? '' : ' ".$ext"'}', hint: 'one of: ${(imageShowExtensions.toList()..sort()).join(', ')}');
}
// Resolve to a concrete file before publishing, so the CLI fails honestly on
@@ -114,7 +102,7 @@ Future<IpcResponse> _show(
if (caption != null && caption.trim().isNotEmpty) 'caption': caption.trim(),
if (fullscreen) 'fullscreen': true,
});
return IpcResponse.ok(id: req.id, data: {'path': resolved, if (caption != null) 'caption': caption, 'fullscreen': fullscreen, 'shown': true});
return IpcResponse.ok(id: req.id, data: {'path': resolved, 'caption': ?caption, 'fullscreen': fullscreen, 'shown': true});
}
/// Lower-cased extension (without the dot) of [path], or '' if none.
+29 -44
View File
@@ -36,10 +36,23 @@ void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry, {ViewPaneSo
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), 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.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);
// pane.tail is a streaming/no-op verb (events arrive via the tail stream),
// a poor request/response MCP tool — keep it off the MCP surface (D-86).
@@ -47,23 +60,14 @@ void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry, {ViewPaneSo
}
IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: message,
hint: hint,
),
);
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,
),
);
id: id,
error: IpcError(code: IpcExitCode.notFound, kind: IpcErrorKind.notFound, message: message),
);
Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry) async {
final args = req.args;
@@ -87,9 +91,7 @@ Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry) async {
final envArg = args['env'];
Map<String, String>? env;
if (envArg is Map) {
env = {
for (final e in envArg.entries) '${e.key}': '${e.value}',
};
env = {for (final e in envArg.entries) '${e.key}': '${e.value}'};
}
try {
@@ -108,29 +110,17 @@ Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry) async {
if (errno != null) {
return IpcResponse.err(
id: req.id,
error: errnoToIpcError(
errno: errno,
op: 'pane.spawn',
target: argv.isNotEmpty ? argv.first : null,
),
error: errnoToIpcError(errno: errno, op: 'pane.spawn', target: argv.isNotEmpty ? argv.first : null),
);
}
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'pane.spawn failed: ${e.message}',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'pane.spawn failed: ${e.message}'),
);
} catch (e) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'pane.spawn failed: $e',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'pane.spawn failed: $e'),
);
}
}
@@ -143,7 +133,7 @@ Future<IpcResponse> _list(IpcRequest req, PaneRegistry registry, ViewPaneSource?
for (final p in registry.panes) p.toJson(),
if (viewPanes != null)
for (final v in viewPanes()) v.toJson(),
]
],
},
);
}
@@ -198,12 +188,7 @@ Future<IpcResponse> _focus(IpcRequest req, PaneRegistry registry) async {
if (registry.get(id) == null) return _notFound(req.id, 'no such pane: $id');
// Focus is advisory on the daemon side — UIs track their own focus
// state. We just emit the event so subscribers know what changed.
registry.events.emit(IpcEvent(
subsystem: 'pane',
kind: 'pane.focused',
timestamp: DateTime.now().toUtc(),
data: {'id': id},
));
registry.events.emit(IpcEvent(subsystem: 'pane', kind: 'pane.focused', timestamp: DateTime.now().toUtc(), data: {'id': id}));
return IpcResponse.ok(id: req.id, data: {'id': id});
}
+8 -23
View File
@@ -86,37 +86,22 @@ Future<IpcResponse> _resize(IpcRequest req, PanelResizer r) async {
} else {
r.bumpEditorRatio(value);
}
return IpcResponse.ok(id: req.id, data: {
'slot': slot,
'ratio': r.currentEditorRatio,
});
return IpcResponse.ok(id: req.id, data: {'slot': slot, 'ratio': r.currentEditorRatio});
}
final ok = 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),
});
return IpcResponse.ok(id: req.id, data: {'slot': slot, 'size': r.currentSlotSize(slot)});
}
IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: message,
hint: hint,
),
);
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,
),
);
id: id,
error: IpcError(code: IpcExitCode.notFound, kind: IpcErrorKind.notFound, message: message),
);
+8 -31
View File
@@ -128,11 +128,7 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
d.register('pql.decisions.list', (req) async {
try {
final results = await pql.decisionList(
type: req.args['type'] as String?,
domain: req.args['domain'] as String?,
status: req.args['status'] as String?,
);
final results = await pql.decisionList(type: req.args['type'] as String?, domain: req.args['domain'] as String?, status: req.args['status'] as String?);
return IpcResponse.ok(id: req.id, data: {'decisions': results});
} on PqlException catch (e) {
return _pqlError(req.id, e);
@@ -158,11 +154,7 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
return _userError(req.id, 'pql.decisions.show requires an id');
}
try {
final result = await pql.decisionShow(
id,
withRefs: req.args['withRefs'] as bool? ?? false,
withTickets: req.args['withTickets'] as bool? ?? false,
);
final result = await pql.decisionShow(id, withRefs: req.args['withRefs'] as bool? ?? false, withTickets: req.args['withTickets'] as bool? ?? false);
return IpcResponse.ok(id: req.id, data: result);
} on PqlException catch (e) {
return _pqlError(req.id, e);
@@ -189,11 +181,7 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
return _userError(req.id, 'pql.tickets.show requires an id');
}
try {
final result = await pql.ticketShow(
id,
withContext: req.args['withContext'] as bool? ?? false,
withBlockers: req.args['withBlockers'] as bool? ?? false,
);
final result = await pql.ticketShow(id, withContext: req.args['withContext'] as bool? ?? false, withBlockers: req.args['withBlockers'] as bool? ?? false);
return IpcResponse.ok(id: req.id, data: result);
} on PqlException catch (e) {
return _pqlError(req.id, e);
@@ -205,8 +193,8 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
final ids = rawIds is List
? rawIds.cast<String>()
: rawIds is String
? [rawIds]
: <String>[];
? [rawIds]
: <String>[];
final status = req.args['status'] as String?;
if (ids.isEmpty || status == null || status.isEmpty) {
return _userError(req.id, 'pql.tickets.status requires ids and status');
@@ -221,9 +209,7 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
d.register('pql.tickets.board', (req) async {
try {
final board = await pql.ticketBoard(
team: req.args['team'] as String?,
);
final board = await pql.ticketBoard(team: req.args['team'] as String?);
return IpcResponse.ok(id: req.id, data: {'columns': board});
} on PqlException catch (e) {
return _pqlError(req.id, e);
@@ -243,22 +229,13 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
IpcResponse _userError(String id, String message) {
return IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: message,
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message),
);
}
IpcResponse _pqlError(String id, PqlException e) {
return IpcResponse.err(
id: id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: e.message,
hint: e.stderr.isNotEmpty ? e.stderr : null,
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: e.message, hint: e.stderr.isNotEmpty ? e.stderr : null),
);
}
+8 -45
View File
@@ -25,12 +25,7 @@ import 'dispatcher.dart';
/// sink. One instance per workspace, constructed alongside the other
/// daemon services.
class SearchService {
SearchService({
required this.root,
required this.ignore,
required this.events,
this.useIsolates = true,
});
SearchService({required this.root, required this.ignore, required this.events, this.useIsolates = true});
final Directory root;
final IgnoreSet ignore;
@@ -64,12 +59,7 @@ class SearchService {
/// path-safety guard. The clean-git-tree safety gate is enforced by
/// the caller (the UI checks `git.status` before requesting apply).
Future<Map<String, Object?>> replace(SearchQuery query, String replacement, {required bool apply}) async {
final files = await computeReplacements(
root: root,
ignore: ignore,
query: query,
replacement: replacement,
);
final files = await computeReplacements(root: root, ignore: ignore, query: query, replacement: replacement);
if (!apply) {
return {
'apply': false,
@@ -103,13 +93,7 @@ class SearchService {
Future<void> _run(String id, SearchQuery query, CancelToken cancel) async {
try {
await for (final batch in grepWorkspace(
root: root,
ignore: ignore,
query: query,
cancel: cancel,
useIsolates: useIsolates,
)) {
await for (final batch in grepWorkspace(root: root, ignore: ignore, query: query, cancel: cancel, useIsolates: useIsolates)) {
if (cancel.isCancelled) break;
_emit('search.match', {
'searchId': id,
@@ -127,12 +111,7 @@ class SearchService {
}
void _emit(String kind, Map<String, Object?> data) {
events.emit(IpcEvent(
subsystem: 'search',
kind: kind,
timestamp: DateTime.now().toUtc(),
data: data,
));
events.emit(IpcEvent(subsystem: 'search', kind: kind, timestamp: DateTime.now().toUtc(), data: data));
}
}
@@ -166,11 +145,7 @@ void registerSearchCommands(DaemonDispatcher d, SearchService search) {
if (query.pattern.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'search.grep requires a non-empty pattern',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'search.grep requires a non-empty pattern'),
);
}
final id = search.start(query);
@@ -184,11 +159,7 @@ void registerSearchCommands(DaemonDispatcher d, SearchService search) {
if (query.pattern.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'search.replace requires a non-empty pattern',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'search.replace requires a non-empty pattern'),
);
}
try {
@@ -197,11 +168,7 @@ void registerSearchCommands(DaemonDispatcher d, SearchService search) {
} on FormatException catch (e) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'invalid regex: ${e.message}',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'invalid regex: ${e.message}'),
);
}
}, schema: _replaceSchema);
@@ -211,11 +178,7 @@ void registerSearchCommands(DaemonDispatcher d, SearchService search) {
if (id == null || id.isEmpty) {
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: 'search.cancel requires a searchId',
),
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: 'search.cancel requires a searchId'),
);
}
search.cancel(id);
+7 -22
View File
@@ -47,27 +47,20 @@ const Set<String> _toastSeverities = {'success', 'warning', 'error', 'info'};
/// nothing has reported a value for that address (or there is no live UI).
typedef FilterValueSource = String? Function(String address);
void registerUiCommands(
DaemonDispatcher d,
MessagePublisher? Function() publisher, {
FilterValueSource? filterValue,
}) {
void registerUiCommands(DaemonDispatcher d, MessagePublisher? Function() publisher, {FilterValueSource? filterValue}) {
d.register('ui.open', (req) async => _open(req, publisher));
d.register('ui.toast', (req) async => _toast(req, publisher));
d.register(
'ui.filter',
(req) async => _filter(req, publisher, filterValue),
schema: const CommandSchema(
positional: ['address', 'query'],
args: {'address': ArgSpec(required: true), 'query': ArgSpec()},
),
schema: const CommandSchema(positional: ['address', 'query'], args: {'address': ArgSpec(required: true), 'query': ArgSpec()}),
);
}
IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err(
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
);
id: id,
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
);
Future<IpcResponse> _open(IpcRequest req, MessagePublisher? Function() publisherSource) async {
// Accept CLI positionals (`ui open <reader> <ref>`) or named args.
@@ -134,11 +127,7 @@ Future<IpcResponse> _toast(IpcRequest req, MessagePublisher? Function() publishe
);
}
// Channel literal must match ToastService's `toastChannel`.
publish('cli', 'toast', {
'message': message,
'severity': severity,
if (durationMs != null) 'durationMs': durationMs,
});
publish('cli', 'toast', {'message': message, 'severity': severity, 'durationMs': ?durationMs});
return IpcResponse.ok(id: req.id, data: {'message': message, 'severity': severity, 'shown': true});
}
@@ -154,11 +143,7 @@ Future<IpcResponse> _toast(IpcRequest req, MessagePublisher? Function() publishe
/// `address` is a pane/box id from `clide pane list`. With a `query` arg the
/// verb *drives* — publishes a `filter.set` the box consumes. Without one it
/// *observes* — reads the box's last reported value from the FilterStateCache.
Future<IpcResponse> _filter(
IpcRequest req,
MessagePublisher? Function() publisherSource,
FilterValueSource? filterValue,
) async {
Future<IpcResponse> _filter(IpcRequest req, MessagePublisher? Function() publisherSource, FilterValueSource? filterValue) async {
final address = req.args['address'] as String?;
if (address == null || address.isEmpty) {
return _userErr(req.id, 'an address is required', hint: 'a pane/box id from `clide pane list` (e.g. decisions.panel)');