Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.9.20] - 2026-08-01

### Added

- 💬 **Chat settings have their own Admin page.** Admins can now turn chat title generation on or off, choose the models used for titles, summaries, and approval reviews, and adjust when long chats are compacted.
- 🗂️ **Git settings have their own Admin page.** Admins can now choose which model drafts commit messages.
- 🧠 **Memory and skills reviews can use a chosen model.** Background reviews can now use a specific model, or keep following the current chat model when left unset.

### Changed

- 🧭 **Model choices are clearer for helper features.** Model pickers now show when a feature is using the current or default model instead of forcing a saved choice.
- ⚙️ **Admin settings are easier to scan.** Chat and Git options moved out of Models into dedicated pages, leaving model setup focused on model setup.

### Fixed

- 🤖 **Auto mode asks at smarter moments.** Computer can now approve safe, expected tool use in Auto mode and still asks when an action looks risky or unclear.

## [0.9.19] - 2026-07-31

### Changed
Expand Down
1 change: 1 addition & 0 deletions cptr/frontend/src/lib/apis/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type MemorySettings = {
enabled: boolean;
tool_enabled: boolean;
background_review_enabled: boolean;
'background_review.model'?: string | null;
review_interval_turns: number;
user_char_limit: number;
workspace_char_limit: number;
Expand Down
190 changes: 190 additions & 0 deletions cptr/frontend/src/lib/components/Admin/Chat.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<script lang="ts">
import { onMount } from 'svelte';
import { toast } from 'svelte-sonner';
import { getAdminConfig, updateConfig } from '$lib/apis/admin';
import { t } from '$lib/i18n';
import Spinner from '$lib/components/common/Spinner.svelte';
import ToggleSwitch from '$lib/components/common/ToggleSwitch.svelte';
import ModelSelector from '$lib/components/common/ModelSelector.svelte';

let loading = $state(true);
let saving = $state(false);
let titleGenerationEnabled = $state(true);
let titleGenerationModel = $state<string | null>(null);
let contextCompactionModel = $state<string | null>(null);
let toolApprovalReviewModel = $state<string | null>(null);
let compactTokenThreshold = $state(80000);

onMount(async () => {
try {
const config = await getAdminConfig();
titleGenerationEnabled =
config['chat.title_generation.enabled'] !== false &&
config['chat.title_generation.enabled'] !== 'false';
titleGenerationModel =
typeof config['chat.title_generation.model'] === 'string'
? config['chat.title_generation.model']
: null;
contextCompactionModel =
typeof config['chat.context_compaction.model'] === 'string'
? config['chat.context_compaction.model']
: null;
toolApprovalReviewModel =
typeof config['tool_approval.review.model'] === 'string'
? config['tool_approval.review.model']
: null;
compactTokenThreshold = Number(config['chat.compact_token_threshold']) || 80000;
} catch {
toast.error($t('admin.failedToLoadConfig'));
} finally {
loading = false;
}
});

async function save() {
saving = true;
try {
await updateConfig({
'chat.title_generation.enabled': titleGenerationEnabled,
'chat.title_generation.model': titleGenerationModel,
'chat.context_compaction.model': contextCompactionModel,
'tool_approval.review.model': toolApprovalReviewModel,
'chat.compact_token_threshold': Math.max(10000, Number(compactTokenThreshold) || 80000)
});
toast.success($t('settings.saved'));
} catch {
toast.error($t('admin.failedToSave'));
} finally {
saving = false;
}
}
</script>

<div class="flex flex-col h-full">
{#if loading}
<div class="flex justify-center py-8"><Spinner size={16} /></div>
{:else}
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5">
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">{$t('admin.chat')}</h2>

<h3 class="text-xs text-gray-400 dark:text-gray-600 mb-2">
{$t('admin.chatTitles')}
</h3>
<div class="flex flex-col gap-2.5">
<label class="flex items-center justify-between cursor-pointer">
<span class="text-xs text-gray-600 dark:text-gray-400">
{$t('admin.chatTitleGeneration')}
</span>
<ToggleSwitch
value={titleGenerationEnabled}
onchange={(value) => {
titleGenerationEnabled = value;
}}
/>
</label>
<p class="text-[0.6875rem] text-gray-400 dark:text-gray-600 -mt-1">
{$t('admin.chatTitleGenerationHint')}
</p>

{#if titleGenerationEnabled}
<div>
<div class="flex items-center justify-between gap-3">
<span class="min-w-0 text-xs text-gray-600 dark:text-gray-400">
{$t('admin.chatTitleModel')}
</span>
<div class="shrink-0">
<ModelSelector
bind:selectedModel={titleGenerationModel}
nullable
nullLabel={$t('modelSelector.currentModel')}
preferAbove={false}
/>
</div>
</div>
<p class="text-[0.6875rem] text-gray-400 dark:text-gray-600 -mt-1">
{$t('admin.chatTitleModelHint')}
</p>
</div>
{/if}
</div>

<h3 class="text-xs text-gray-400 dark:text-gray-600 mb-2 mt-5">
{$t('admin.contextCompaction')}
</h3>
<div class="flex flex-col gap-2.5">
<div>
<div class="flex items-center justify-between gap-3">
<span class="min-w-0 text-xs text-gray-600 dark:text-gray-400">
{$t('admin.contextSummaryModel')}
</span>
<div class="shrink-0">
<ModelSelector
bind:selectedModel={contextCompactionModel}
nullable
nullLabel={$t('modelSelector.currentModel')}
preferAbove={false}
/>
</div>
</div>
<p class="text-[0.6875rem] text-gray-400 dark:text-gray-600 -mt-1">
{$t('admin.contextSummaryModelHint')}
</p>
</div>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="compact-threshold">
{$t('admin.compactTokenThreshold')}
</label>
<div class="flex items-center gap-1.5 mt-1">
<input
id="compact-threshold"
type="number"
bind:value={compactTokenThreshold}
min="10000"
max="1000000"
step="10000"
class="w-24 h-7 px-2 rounded-lg text-xs bg-gray-100 dark:bg-white/6 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-white/8 outline-none focus:border-blue-400 dark:focus:border-blue-500 transition-colors"
/>
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$t('admin.compactTokenThresholdUnit')}
</span>
</div>
<p class="text-[0.6875rem] text-gray-400 dark:text-gray-600 mt-0.5">
{$t('admin.compactTokenThresholdHint')}
</p>
</div>
</div>

<h3 class="text-xs text-gray-400 dark:text-gray-600 mb-2 mt-5">
{$t('admin.toolApproval')}
</h3>
<div>
<div class="flex items-center justify-between gap-3">
<span class="min-w-0 text-xs text-gray-600 dark:text-gray-400">
{$t('admin.toolApprovalReviewModel')}
</span>
<div class="shrink-0">
<ModelSelector
bind:selectedModel={toolApprovalReviewModel}
nullable
nullLabel={$t('modelSelector.currentModel')}
preferAbove={false}
/>
</div>
</div>
<p class="text-[0.6875rem] text-gray-400 dark:text-gray-600 -mt-1">
{$t('admin.toolApprovalReviewModelHint')}
</p>
</div>
</div>

<div class="shrink-0 pt-3 flex justify-end">
<button
class="text-[0.8125rem] text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors duration-100 disabled:opacity-50"
disabled={saving}
onclick={save}
>
{saving ? $t('settings.saving') : $t('settings.save')}
</button>
</div>
{/if}
</div>
77 changes: 77 additions & 0 deletions cptr/frontend/src/lib/components/Admin/Git.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script lang="ts">
import { onMount } from 'svelte';
import { toast } from 'svelte-sonner';
import { getAdminConfig, updateConfig } from '$lib/apis/admin';
import { t } from '$lib/i18n';
import Spinner from '$lib/components/common/Spinner.svelte';
import ModelSelector from '$lib/components/common/ModelSelector.svelte';

let loading = $state(true);
let saving = $state(false);
let commitMessageModel = $state<string | null>(null);

onMount(async () => {
try {
const config = await getAdminConfig();
commitMessageModel =
typeof config['git.commit_message_generation.model'] === 'string'
? config['git.commit_message_generation.model']
: null;
} catch {
toast.error($t('admin.failedToLoadConfig'));
} finally {
loading = false;
}
});

async function save() {
saving = true;
try {
await updateConfig({ 'git.commit_message_generation.model': commitMessageModel });
toast.success($t('settings.saved'));
} catch {
toast.error($t('admin.failedToSave'));
} finally {
saving = false;
}
}
</script>

<div class="flex flex-col h-full">
{#if loading}
<div class="flex justify-center py-8"><Spinner size={16} /></div>
{:else}
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5">
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">{$t('admin.git')}</h2>

<div>
<div class="flex items-center justify-between gap-3">
<span class="min-w-0 text-xs text-gray-600 dark:text-gray-400">
{$t('admin.gitCommitMessageModel')}
</span>
<div class="shrink-0">
<ModelSelector
bind:selectedModel={commitMessageModel}
nullable
nullLabel={$t('modelSelector.defaultModel')}
preferAbove={false}
/>
</div>
</div>
<p class="text-[0.6875rem] text-gray-400 dark:text-gray-600 -mt-1">
{$t('admin.gitCommitMessageModelHint')}
</p>
</div>
</div>

<div class="shrink-0 pt-3 flex justify-end">
<button
class="text-[0.8125rem] text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors duration-100 disabled:opacity-50"
disabled={saving}
onclick={save}
>
{saving ? $t('settings.saving') : $t('settings.save')}
</button>
</div>
{/if}
</div>
Loading
Loading