feat(settings): Claude category — new-session defaults (T-457)

A Settings → Claude category sets per-user defaults for NEW sessions: model,
effort, and permission mode (generic schema selects persisting
app.claude.default*). The pane reads them at spawn — effort flows through the
existing --effort flag (SpawnSpec.effort); model and permission mode are sent
as control requests right after a fresh (non-resume, non-fork) session starts.
'default'/unset values are no-ops, leaving the CLI's own defaults.

The optional "settings changed — apply to current sessions?" prompt is filed
as T-470: it needs a custom control (a generic select can't prompt), so it's
a clean follow-up on the T-452 escape hatch.

Tests: defaultEffortFlag sentinel handling; the category contributes the three
default fields. The thin pane-side applySessionDefaults is covered by the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 19:58:11 +02:00
co-authored by Claude Opus 4.8
parent d9eadf295a
commit ea670fd077
8 changed files with 147 additions and 0 deletions
@@ -6,6 +6,7 @@
library;
import 'package:clide/builtin/claude/src/activity_cluster.dart' show kActivityFoldLevelKey;
import 'package:clide/builtin/claude/src/session_defaults.dart' show kDefaultEffortKey, kDefaultModelKey, kDefaultPermissionModeKey;
import 'package:clide/builtin/claude/src/claude_config.dart' show activeClaudeConfig;
import 'package:clide/builtin/claude/src/extension.dart';
import 'package:clide/builtin/claude/src/session_orchestrator.dart' show activeSessionOrchestrator;
@@ -157,4 +158,18 @@ void main() {
expect(field.options.map((o) => o.value), containsAll(['none', 'tools', 'thinking', 'everything']));
});
});
group('Claude settings category (T-457)', () {
final category = ext.contributions.whereType<SettingsCategoryContribution>().firstWhere((c) => c.id == 'claude').category;
test('contributes new-session default fields for model, effort, permission', () {
expect(category.title, 'Claude');
final keys = category.sections.expand((s) => s.fields).map((f) => f.key).toSet();
expect(keys, containsAll([kDefaultModelKey, kDefaultEffortKey, kDefaultPermissionModeKey]));
for (final f in category.sections.expand((s) => s.fields)) {
expect(f.kind, SettingsFieldKind.select);
expect(f.options, isNotEmpty);
}
});
});
}
@@ -0,0 +1,36 @@
import 'dart:io';
import 'package:clide/builtin/claude/src/session_defaults.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
late Directory appDir;
late SettingsStore settings;
setUp(() async {
appDir = await Directory.systemTemp.createTemp('clide_cd_');
settings = SettingsStore(appDir: appDir);
await settings.load();
});
tearDown(() async {
settings.dispose();
await appDir.delete(recursive: true);
});
group('defaultEffortFlag (T-457)', () {
test('null when unset — let the CLI default stand', () {
expect(defaultEffortFlag(settings), isNull);
});
test("null for the 'default' sentinel", () async {
await settings.set(kDefaultEffortKey, 'default');
expect(defaultEffortFlag(settings), isNull);
});
test('returns a concrete level', () async {
await settings.set(kDefaultEffortKey, 'high');
expect(defaultEffortFlag(settings), 'high');
});
});
}