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
@@ -5915,3 +5915,6 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15TY946689VZK3MNABSMDW', 'status', 'in_progress', 'done', NULL, '2026-06-17 11:43:37', '2026-06-17 11:43:37.615', '2026-06-17 11:43:37.615', NULL, 'a2f67644a293f12da9b0cdf28dc79055', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'status', 'backlog', 'in_progress', NULL, '2026-06-17 12:08:37', '2026-06-17 12:08:37.084', '2026-06-17 12:08:37.084', NULL, '56dfbd21ec4ea2add7d34c1008d3690b', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'status', 'in_progress', 'ready', NULL, '2026-06-17 12:09:28', '2026-06-17 12:09:28.013', '2026-06-17 12:09:28.013', NULL, '04a42a6bb8bed47fda6da86038dfb15e', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'status', 'ready', 'in_progress', NULL, '2026-06-17 12:11:21', '2026-06-17 12:11:21.874', '2026-06-17 12:11:21.874', NULL, '20c4f05dc5ffc8a29a0e6c2f94dfaf4b', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDB1VFADV4QK1YG29T658T8C', 'description', NULL, 'Follow-up to T-457. The Claude settings category sets new-session defaults (model/effort/permission) via generic schema selects — those persist + seed NEW sessions but don''t touch running ones. Add the ''Claude settings changes detected — apply to current sessions?'' prompt: on a default change with ≥1 running session, confirm via DialogRouter, then apply to all sessions through the existing builtin.claude/command channel (/model, /effort, /permissions). Needs a custom control (the generic select can''t prompt), so swap the Claude category''s selects for a custom-control widget per D-? / the T-452 escape hatch.', NULL, '2026-06-17 12:21:31', '2026-06-17 12:21:31.456', '2026-06-17 12:21:31.456', NULL, '799b77b6ec7862c856957d948326cffd', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'status', 'in_progress', 'done', NULL, '2026-06-17 12:21:39', '2026-06-17 12:21:39.311', '2026-06-17 12:21:39.311', NULL, 'dd983e8a5ccfae1b0dea46a8b5e78249', 2) ON CONFLICT(hash) DO NOTHING;
+4
View File
@@ -7183,3 +7183,7 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15TY946689VZK3MNABSMDW', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Extensions settings category', 'List installed extensions and surface each extension''s registered settings (ext.<id>.* — the extension-owned config pattern from ui-design theme.md); per-extension enable/disable + configure. Reuses the field renderer; extensions register their schema the same way core subsystems do.', 'done', 'medium', NULL, NULL, NULL, '2026-06-16 13:20:41', '2026-06-17 11:43:37.614', NULL, '09f569e8ad465363d95821413ff7d027', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Claude settings category — mirror the sidebar Config panel', 'Mirror lib/builtin/claude/src/meta_sidebar/config_tab.dart in the settings modal: SETTINGS controls (model / effort / permission mode / output style via SettingControlRow publishing the slash command on the live session, D-6 / T-414) plus carded config lists (Skills / Agents / Commands / Hooks / Permissions / MCP Servers) that open their .md (T-183). Reuse the data the sidebar already loads (ClaudeConfig). Wireframe: docs/design/wireframes/settings/settings-claude.png.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-16 13:20:52', '2026-06-17 12:08:37.084', NULL, 'e7198c66868b8d602168083dcd40dda9', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Claude settings category — mirror the sidebar Config panel', 'Mirror lib/builtin/claude/src/meta_sidebar/config_tab.dart in the settings modal: SETTINGS controls (model / effort / permission mode / output style via SettingControlRow publishing the slash command on the live session, D-6 / T-414) plus carded config lists (Skills / Agents / Commands / Hooks / Permissions / MCP Servers) that open their .md (T-183). Reuse the data the sidebar already loads (ClaudeConfig). Wireframe: docs/design/wireframes/settings/settings-claude.png.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-16 13:20:52', '2026-06-17 12:09:28.013', NULL, '8c8ad599dcbe3b500f710b5b03b0c1fb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Claude settings category — mirror the sidebar Config panel', 'Mirror lib/builtin/claude/src/meta_sidebar/config_tab.dart in the settings modal: SETTINGS controls (model / effort / permission mode / output style via SettingControlRow publishing the slash command on the live session, D-6 / T-414) plus carded config lists (Skills / Agents / Commands / Hooks / Permissions / MCP Servers) that open their .md (T-183). Reuse the data the sidebar already loads (ClaudeConfig). Wireframe: docs/design/wireframes/settings/settings-claude.png.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-16 13:20:52', '2026-06-17 12:11:21.874', NULL, 'aa269d123343566d0025e9eee2662ab4', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDB1VFADV4QK1YG29T658T8C', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Claude defaults: ''apply to current sessions?'' prompt on change', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 12:21:23.411', '2026-06-17 12:21:23.411', NULL, '924eef1841e09a2a49c1c39d9bfc1331', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDB1VFADV4QK1YG29T658T8C', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Claude defaults: ''apply to current sessions?'' prompt on change', 'Follow-up to T-457. The Claude settings category sets new-session defaults (model/effort/permission) via generic schema selects — those persist + seed NEW sessions but don''t touch running ones. Add the ''Claude settings changes detected — apply to current sessions?'' prompt: on a default change with ≥1 running session, confirm via DialogRouter, then apply to all sessions through the existing builtin.claude/command channel (/model, /effort, /permissions). Needs a custom control (the generic select can''t prompt), so swap the Claude category''s selects for a custom-control widget per D-? / the T-452 escape hatch.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 12:21:23.411', '2026-06-17 12:21:31.456', NULL, '61f297cc5f01d6f6e43e1fa64efd4875', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15W7SBVAD3QR5NVE67PKWR', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Claude settings category — mirror the sidebar Config panel', 'Mirror lib/builtin/claude/src/meta_sidebar/config_tab.dart in the settings modal: SETTINGS controls (model / effort / permission mode / output style via SettingControlRow publishing the slash command on the live session, D-6 / T-414) plus carded config lists (Skills / Agents / Commands / Hooks / Permissions / MCP Servers) that open their .md (T-183). Reuse the data the sidebar already loads (ClaudeConfig). Wireframe: docs/design/wireframes/settings/settings-claude.png.', 'done', 'medium', NULL, NULL, NULL, '2026-06-16 13:20:52', '2026-06-17 12:21:39.311', NULL, '2f3f27095398a0a0e0240b2e0eac34c2', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+4
View File
@@ -61,6 +61,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
toggling extensions arrives with third-party (Lua) support; built-ins stay
always-on for now. (T-456)
- **Settings → Claude category.** New-session defaults — model, effort, and
permission mode — seed fresh sessions (effort via `--effort` at spawn;
model and permission applied right after start). (T-457)
### Changed
- **Theme picker relabelled "Theme…".** The ⌘K theme picker's command title
+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);
}
@@ -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');
});
});
}