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
+11
View File
@@ -16,6 +16,7 @@ import 'conversation_controller.dart';
import 'conversation_view.dart';
import 'model_picker_card.dart';
import 'permission_mode_control.dart';
import 'session_defaults.dart';
import 'prompt_card.dart';
import 'session_index.dart';
import 'session_naming.dart';
@@ -294,6 +295,11 @@ class _ClaudePaneState extends State<ClaudePane> {
Future<void> _spawn() async {
if (!mounted) return;
// New sessions inherit the user's Claude defaults (T-457): effort flows
// through --effort below; model + permission mode are applied post-spawn.
final settings = ClideKernel.of(context).settings;
_effort ??= defaultEffortFlag(settings);
var isNewSession = false;
final ipc = _ipc();
if (ipc == null || !ipc.isConnected) {
setState(() => _error = 'Daemon not connected.');
@@ -370,11 +376,16 @@ class _ClaudePaneState extends State<ClaudePane> {
return;
}
if (!mounted) return;
isNewSession = !resume;
setState(() => _statusLine = resume ? 'resumed · $_sessionId' : 'new session · $_sessionId');
}
_session = managed.session;
_conversation = managed.conversation;
// A brand-new session (not a resume or fork) starts on the user's default
// model + permission mode (T-457). Sent as control requests; the CLI
// applies them after init. 'default'/unset values are no-ops.
if (isNewSession) applySessionDefaults(managed.session, settings);
// The wire never reports effort — record what this session was spawned
// with so the status line / sidebar can show it (T-412).
if (_effort != null) managed.session.noteEffort(_effort!);
+45
View File
@@ -10,7 +10,9 @@ import 'package:clide/builtin/claude/src/claude_session_host.dart';
import 'package:clide/builtin/claude/src/session_orchestrator.dart';
import 'package:clide/builtin/claude/src/pane_context_status.dart';
import 'package:clide/builtin/claude/src/claude_meta_sidebar.dart';
import 'package:clide/builtin/claude/src/session_defaults.dart';
import 'package:clide/builtin/claude/src/session_index.dart';
import 'package:clide/builtin/claude/src/stream_json_session.dart' show kEffortLevels, kFallbackModels, kPermissionModes;
import 'package:clide/builtin/claude/src/session_storage.dart';
import 'package:clide/builtin/claude/src/ticket_pick_up.dart';
import 'package:clide/builtin/claude/src/transcript_reader.dart' show ImageMessage;
@@ -132,6 +134,49 @@ class ClaudeExtension extends ClideExtension {
],
),
),
// Claude settings category (T-457) — defaults applied to NEW sessions
// (the pane reads these keys at spawn). Effort flows through --effort;
// model + permission mode are sent as control requests post-spawn.
SettingsCategoryContribution(
id: 'claude',
category: SettingsCategory(
id: 'claude',
title: 'Claude',
iconName: 'sparkle',
priority: 40,
sections: [
SettingsSection(
label: 'New session defaults',
fields: [
SettingsField(
key: kDefaultModelKey,
kind: SettingsFieldKind.select,
label: 'Model',
help: 'Model for new sessions.',
defaultValue: 'default',
options: [for (final m in kFallbackModels) SettingsOption(value: m.value, label: m.displayName)],
),
SettingsField(
key: kDefaultEffortKey,
kind: SettingsFieldKind.select,
label: 'Effort',
help: 'Reasoning effort for new sessions (applied via --effort at spawn).',
defaultValue: 'high',
options: [for (final l in kEffortLevels) SettingsOption(value: l.value, label: l.displayName)],
),
SettingsField(
key: kDefaultPermissionModeKey,
kind: SettingsFieldKind.select,
label: 'Permission mode',
help: 'Starting permission mode for new sessions.',
defaultValue: 'default',
options: [for (final p in kPermissionModes) SettingsOption(value: p.value, label: p.displayName)],
),
],
),
],
),
),
// T-171: agent roster controls (D-6 CLI/UI parity).
// Usage: clide claude.agent.show <sessionId>
CommandContribution(
@@ -0,0 +1,29 @@
import 'package:clide/builtin/claude/src/stream_json_session.dart';
import 'package:clide/kernel/kernel.dart';
/// Per-user defaults applied to NEW Claude sessions (T-457). Set from the
/// Settings → Claude category; read by the pane when it spawns a session.
/// Effort is applied at spawn (the `--effort` flag — there's no live
/// set_effort); model and permission mode are sent as control requests right
/// after the session starts.
const String kDefaultModelKey = 'app.claude.defaultModel';
const String kDefaultEffortKey = 'app.claude.defaultEffort';
const String kDefaultPermissionModeKey = 'app.claude.defaultPermissionMode';
/// The default effort for new sessions, or null to let the CLI's own default
/// stand. `'default'` is treated as "no override" too.
String? defaultEffortFlag(SettingsStore settings) {
final v = settings.get<String>(kDefaultEffortKey);
if (v == null || v.isEmpty || v == 'default') return null;
return v;
}
/// Apply the model + permission-mode defaults to a freshly-spawned [session].
/// A null/empty/`'default'` value is a no-op — the CLI's own default stands.
/// (Effort is handled at spawn via [defaultEffortFlag], not here.)
void applySessionDefaults(StreamJsonSession session, SettingsStore settings) {
final model = settings.get<String>(kDefaultModelKey);
if (model != null && model.isNotEmpty && model != 'default') session.setModel(model);
final perm = settings.get<String>(kDefaultPermissionModeKey);
if (perm != null && perm.isNotEmpty && perm != 'default') session.setPermissionMode(perm);
}