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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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()],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)');
|
||||
|
||||
Reference in New Issue
Block a user