Files
clide/lib/builtin/output/src/output_controller.dart
T
jpmschweitzerandClaude Opus 4.8 432a5f2d5a feat(log): live verbosity toggle — dock Level chip + clide log level (T-433)
The boot-time toggle (CLIDE_LOG / app.log.level) existed; this makes it
adjustable at runtime, from the two surfaces that fit — D-6 parity — and NOT a
Claude-composer slash command (log verbosity isn't a Claude-session concept).

- Output dock Level chip: was a view-only filter; now also drives the kernel
  Logger.minLevel and persists app.log.level, so the choice is real (changes
  what's captured) and survives restart. Initialized from the logger's current
  level so it reflects a CLI change. A null callback keeps it a pure view
  filter (tests / no kernel).
- `clide log level [<level>]` (lib/src/daemon/log_commands.dart): no arg reports
  the level + vocabulary; a valid level sets the live logger and persists;
  unknown → bad_arg (code 64), logger untouched. Wired in buildDispatcher with
  the kernel logger + settings (captured post-boot).

Tested: the command (get/set/case-insensitive/bad-arg, live + persist) and the
controller (initial level, callback fires, same-level no-op, filter-only when
unwired). Coverage gate 95.05%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:32:56 +02:00

79 lines
2.4 KiB
Dart

/// Filter state + filtered view over a [LogRing] for the Output dock tab
/// (T-54 / D-87). Notifies when the ring changes or a filter is set, so the
/// view rebuilds; the ring itself stays Flutter-free.
library;
import 'dart:async';
import 'package:clide/kernel/src/log.dart';
import 'package:clide/kernel/src/log_ring.dart';
import 'package:flutter/foundation.dart';
class OutputController extends ChangeNotifier {
OutputController(this.ring, {LogLevel? initialLevel, this.onMinLevelChanged}) : minLevel = initialLevel ?? LogLevel.debug {
_sub = ring.changes.listen((_) => notifyListeners());
}
final LogRing ring;
late final StreamSubscription<void> _sub;
/// Invoked when the Level chip changes the level — the dock chip is the live
/// dev/prod verbosity toggle (T-433), not just a view filter. The owner wires
/// this to set the kernel `Logger.minLevel` and persist `app.log.level`. Null
/// in tests / when no kernel is attached, leaving the chip a pure view filter.
final void Function(LogLevel)? onMinLevelChanged;
/// Minimum level shown — initialized from the kernel logger's level so the
/// chip reflects the real verbosity (which a `clide log level` CLI may have
/// already set), defaulting to debug (trace is firehose-noise).
LogLevel minLevel;
/// Source filter; null = all sources.
String? source;
/// Free-text filter over message + source.
String text = '';
void setMinLevel(LogLevel level) {
if (minLevel == level) return;
minLevel = level;
onMinLevelChanged?.call(level);
notifyListeners();
}
void setSource(String? value) {
if (source == value) return;
source = value;
notifyListeners();
}
void setText(String value) {
if (text == value) return;
text = value;
notifyListeners();
}
/// Clear the underlying ring (the panel's Clear action). The ring's change
/// event drives the rebuild.
void clear() => ring.clear();
/// Records passing the current filters, oldest first.
List<LogRecord> get filtered {
final lf = text.toLowerCase();
return ring.records.where((r) {
if (r.level.index < minLevel.index) return false;
if (source != null && r.source != source) return false;
if (lf.isNotEmpty && !r.message.toLowerCase().contains(lf) && !r.source.toLowerCase().contains(lf)) {
return false;
}
return true;
}).toList();
}
@override
void dispose() {
_sub.cancel();
super.dispose();
}
}