diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b7422e..97c57da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ 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.2.2] - 2026-06-11 + +### Added + +- 🤖 **Per-model configuration.** Set custom parameters (temperature, top_p, etc.) for each model or as global defaults. Per-chat overrides still take priority. +- 🧠 **Automatic context compaction.** Long conversations are automatically summarized to keep things running smoothly, no manual intervention needed. +- ➕ **Per-chat parameters.** Override model parameters for a single chat session from the `+` menu. + +### Changed + +- ⚙️ **Unified settings.** Admin panel merged into Settings. No more separate modal. Admin sections appear automatically for admin users. +- ⌨️ **Shortcut hint in menu.** The Settings menu item now shows its keyboard shortcut. + ## [0.2.1] - 2026-06-11 ### Added diff --git a/cptr/env.py b/cptr/env.py index f0c16c7..6d50a38 100644 --- a/cptr/env.py +++ b/cptr/env.py @@ -27,6 +27,7 @@ ).lower() in ("true", "1", "yes") CHAT_TOOL_MAX_CHARS = int(os.environ.get("CHAT_TOOL_MAX_CHARS", "50000")) CHAT_TOOL_COMMAND_MAX_CHARS = int(os.environ.get("CHAT_TOOL_COMMAND_MAX_CHARS", "8000")) +CHAT_COMPACT_TOKEN_THRESHOLD = int(os.environ.get("CHAT_COMPACT_TOKEN_THRESHOLD", "80000")) # ── AI stream settings ────────────────────────────────────── STREAM_CONNECT_TIMEOUT_SECONDS = float(os.environ.get("CPTR_STREAM_CONNECT_TIMEOUT", "30")) diff --git a/cptr/frontend/src/lib/apis/admin.ts b/cptr/frontend/src/lib/apis/admin.ts index 86735f8..b12eb51 100644 --- a/cptr/frontend/src/lib/apis/admin.ts +++ b/cptr/frontend/src/lib/apis/admin.ts @@ -104,3 +104,27 @@ export const verifyConnection = (id: string) => fetchJSON<{ ok: boolean; message: string }>(`/api/admin/connections/${id}/verify`, { method: 'POST' }); + +// ── Model Config ──────────────────────────────────────────── + +export interface ModelConfigEntry { + is_active?: boolean; + params?: { request_params?: Record }; +} + +export interface ModelConfigResponse { + config: Record; + models: { id: string; name: string; provider: string; connection_id: string }[]; +} + +export const getModelConfig = async (): Promise => + fetchJSON('/api/admin/models/config'); + +export const updateModelConfig = ( + modelId: string, + update: { is_active?: boolean; params?: Record } +) => + fetchJSON(`/api/admin/models/${encodeURIComponent(modelId)}/config`, { + ...jsonBody(update), + method: 'PUT' + }); diff --git a/cptr/frontend/src/lib/apis/chat.ts b/cptr/frontend/src/lib/apis/chat.ts index 0064f90..2ebf53a 100644 --- a/cptr/frontend/src/lib/apis/chat.ts +++ b/cptr/frontend/src/lib/apis/chat.ts @@ -67,7 +67,7 @@ export const sendMessage = ( workspace: string, chatId?: string, parentId?: string | null, - params: { tool_approval_mode?: string } = {}, + params: { tool_approval_mode?: string; plan_mode?: boolean; request_params?: Record } = {}, regenerationPrompt?: string, files?: { id: string; name: string; url: string; type: string }[] ) => diff --git a/cptr/frontend/src/lib/components/Admin/Models.svelte b/cptr/frontend/src/lib/components/Admin/Models.svelte new file mode 100644 index 0000000..4ea41ef --- /dev/null +++ b/cptr/frontend/src/lib/components/Admin/Models.svelte @@ -0,0 +1,227 @@ + + +{#snippet paramRows(rows: ParamRow[], onInput: () => void, onRemove: (i: number) => void, onAdd: () => void)} +
+ request params + {#each rows as row, i} +
+ + + +
+ {/each} + +
+{/snippet} + +
+

{$t('admin.models')}

+ + {#if loading} +
+ {:else} +
+ + + + {#if globalExpanded} + {@render paramRows( + globalRows, + () => (globalDirty = true), + (i) => { globalRows = globalRows.filter((_, idx) => idx !== i); globalDirty = true; }, + () => { globalRows = [...globalRows, { key: '', value: '' }]; globalDirty = true; } + )} + {/if} + + + {#each models as model} + + + {#if selectedId === model.id} + {@render paramRows( + model.rows, + () => (model.dirty = true), + (i) => { model.rows = model.rows.filter((_, idx) => idx !== i); model.dirty = true; }, + () => { model.rows = [...model.rows, { key: '', value: '' }]; model.dirty = true; } + )} + {/if} + {/each} + + {#if models.length === 0} +

{$t('models.noModels')}

+ {/if} +
+ +
+ +
+ {/if} +
+ + diff --git a/cptr/frontend/src/lib/components/AdminPanel.svelte b/cptr/frontend/src/lib/components/AdminPanel.svelte deleted file mode 100644 index a1c3249..0000000 --- a/cptr/frontend/src/lib/components/AdminPanel.svelte +++ /dev/null @@ -1,57 +0,0 @@ - - - - - -
- {#if activeTab === 'users'} - - {:else if activeTab === 'connections'} - - {:else if activeTab === 'settings'} - - {/if} -
-
diff --git a/cptr/frontend/src/lib/components/DropdownMenu.svelte b/cptr/frontend/src/lib/components/DropdownMenu.svelte index 1173bc0..37fc2ce 100644 --- a/cptr/frontend/src/lib/components/DropdownMenu.svelte +++ b/cptr/frontend/src/lib/components/DropdownMenu.svelte @@ -29,6 +29,8 @@ maxHeight?: string; /** Optional header snippet rendered above items (e.g. search input). */ header?: Snippet; + /** Optional snippet rendered when items array is empty. */ + empty?: Snippet; /** Additional CSS classes for the menu container. */ className?: string; /** Horizontal alignment relative to anchor: 'start' (left) or 'end' (right). */ @@ -43,6 +45,7 @@ preferAbove = false, maxHeight, header, + empty, className = '', align = 'start' }: Props = $props(); @@ -148,44 +151,48 @@ {/if}
- {#each items as item} - {#if item.divider} -
- {:else} - - {/if} - {/each} + {#if items.length === 0 && empty} + {@render empty()} + {:else} + {#each items as item} + {#if item.divider} +
+ {:else} + + {/if} + {/each} + {/if}
diff --git a/cptr/frontend/src/lib/components/Icon.svelte b/cptr/frontend/src/lib/components/Icon.svelte index 73cebcd..f240976 100644 --- a/cptr/frontend/src/lib/components/Icon.svelte +++ b/cptr/frontend/src/lib/components/Icon.svelte @@ -310,5 +310,9 @@ {:else if name === 'clock'} + {:else if name === 'cube'} + + + {/if} diff --git a/cptr/frontend/src/lib/components/SettingsModal.svelte b/cptr/frontend/src/lib/components/SettingsModal.svelte index 44fb49a..49d2524 100644 --- a/cptr/frontend/src/lib/components/SettingsModal.svelte +++ b/cptr/frontend/src/lib/components/SettingsModal.svelte @@ -5,16 +5,47 @@ import Account from './Settings/Account.svelte'; import Keyboard from './Settings/Keyboard.svelte'; import About from './Settings/About.svelte'; + import Users from './Admin/Users.svelte'; + import Connections from './Admin/Connections.svelte'; + import Models from './Admin/Models.svelte'; + import AdminSettings from './Admin/Settings.svelte'; + import { session } from '$lib/session'; import { t } from '$lib/i18n'; + type Tab = + | 'general' + | 'keyboard' + | 'account' + | 'about' + | 'users' + | 'connections' + | 'models' + | 'admin_settings'; + interface Props { onclose: () => void; - initialTab?: 'general' | 'keyboard' | 'account' | 'about'; + initialTab?: Tab; } let { onclose, initialTab = 'general' }: Props = $props(); - let activeTab = $state<'general' | 'keyboard' | 'account' | 'about'>(initialTab); + let activeTab = $state(initialTab); + + const isAdmin = $derived($session?.role === 'admin'); + + const personalTabs: { id: Tab; label: string; icon: string }[] = [ + { id: 'general', label: 'General', icon: 'settings' }, + { id: 'keyboard', label: 'Keyboard', icon: 'terminal' }, + { id: 'account', label: 'Account', icon: 'user' }, + { id: 'about', label: 'About', icon: 'info' } + ]; + + const adminTabs: { id: Tab; label: string; icon: string }[] = [ + { id: 'users', label: 'Users', icon: 'user' }, + { id: 'connections', label: 'Connections', icon: 'plug' }, + { id: 'models', label: 'Models', icon: 'cube' }, + { id: 'admin_settings', label: 'Configuration', icon: 'shield' } + ]; {$t('settings.back')} - {#each [{ id: 'general', label: $t('settings.general'), icon: 'settings' }, { id: 'keyboard', label: 'Keyboard', icon: 'terminal' }, { id: 'account', label: $t('settings.account'), icon: 'user' }, { id: 'about', label: $t('settings.about'), icon: 'info' }] as tab} + + + {#each personalTabs as tab} {/each} + + + {#if isAdmin} + + + {#each adminTabs as tab} + + {/each} + {/if} @@ -56,6 +107,14 @@ {:else if activeTab === 'about'} + {:else if activeTab === 'users'} + + {:else if activeTab === 'connections'} + + {:else if activeTab === 'models'} + + {:else if activeTab === 'admin_settings'} + {/if} diff --git a/cptr/frontend/src/lib/components/Sidebar.svelte b/cptr/frontend/src/lib/components/Sidebar.svelte index 346f21e..74f8a44 100644 --- a/cptr/frontend/src/lib/components/Sidebar.svelte +++ b/cptr/frontend/src/lib/components/Sidebar.svelte @@ -21,7 +21,7 @@ import DirectoryPicker from './DirectoryPicker.svelte'; import DropdownMenu from './DropdownMenu.svelte'; import SettingsModal from './SettingsModal.svelte'; - import AdminPanel from './AdminPanel.svelte'; + import { tooltip } from '$lib/tooltip'; import { session, clearSession } from '$lib/session'; import { getWelcome } from '$lib/apis/state'; @@ -37,8 +37,7 @@ let showPicker = $state(false); let showMenu = $state(false); let showSettings = $state(false); - let settingsTab = $state<'general' | 'account' | 'about'>('general'); - let showAdmin = $state(false); + let settingsTab = $state('general'); let wsMenuPath = $state(null); let wsMenuAnchor = $state(null); @@ -508,25 +507,14 @@ image: $session.profile_image_url || '/user.png', onclick: () => { settingsTab = 'account'; + showMenu = false; showSettings = true; } } ] : []), ...($session ? [{ divider: true, label: '', onclick: () => {} }] : []), - { label: $t('sidebar.settings'), icon: 'settings', onclick: openSettings }, - ...($session?.role === 'admin' - ? [ - { - label: $t('sidebar.admin'), - icon: 'shield', - onclick: () => { - showMenu = false; - showAdmin = true; - } - } - ] - : []), + { label: $t('sidebar.settings'), icon: 'settings', shortcut: formatChord($keybindings.openSettings), onclick: openSettings }, { divider: true, label: '', onclick: () => {} }, { label: $t('sidebar.logOut'), icon: 'log-out', onclick: logout } ]} @@ -562,10 +550,6 @@ /> {/if} -{#if showAdmin} - (showAdmin = false)} /> -{/if} -