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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ 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.6.0] - 2026-06-20

### Added

- 🧠 **Memory.** The AI can now remember things about you and your projects. Memories are stored per user and per workspace, and are automatically included in future conversations. You can view, edit, and delete memories from the new Memory tab in Settings. Admins can also turn on background memory review, which lets the AI quietly pick up on preferences and patterns as you chat.
- 🎨 **Image generation and editing.** Ask the AI to create or edit images right from the chat. Generated images are saved to your workspace and displayed inline in the conversation. Supports any OpenAI-compatible image API. Configure from the new Images tab in admin settings.
- 🔀 **Background sub-agents.** Sub-agents can now run in the background. The AI kicks off a task, keeps chatting with you, and brings the results back when the background work is done. Great for long-running research or multi-step tasks you don't want to wait on.
- 🔍 **Better chat search.** Searching your chats now looks at chat IDs, titles, summaries, and message content all at once, with smarter ranking. Exact and prefix matches on titles and IDs show up first, then summary matches, then message content. You can also filter by workspace and choose whether to include sub-agent chats.

### Changed

- 🖼️ **Generated images show inline.** Images created by the AI now appear as proper image previews in the chat instead of raw file paths or JSON. The tool call output also shows a cleaner summary instead of a wall of data.
- 🏷️ **Background sub-agents are labeled.** When the AI runs a task in the background, the tool call label now says "Background sub-agent" so you can tell it apart from a regular sub-agent at a glance.
- 🧹 **Code cleanup.** Formatting and style improvements across the codebase for better readability.

## [0.5.6] - 2026-06-19

### Changed
Expand Down
10 changes: 10 additions & 0 deletions cptr/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
files_router,
gateway_router,
git_router,
images_router,
memory_router,
proxy_router,
search_router,
skills_router,
Expand Down Expand Up @@ -75,6 +77,12 @@ async def shutdown():
bot_manager = getattr(app.state, "bot_manager", None)
if bot_manager:
await bot_manager.stop_all()
try:
from cptr.utils.async_subagents import cancel_all_async_subagents

await cancel_all_async_subagents(reason="shutdown")
except Exception:
pass
# Clean up browser sessions and launched Chrome
try:
from cptr.utils.browser.session import session_manager
Expand Down Expand Up @@ -257,6 +265,8 @@ async def get_config():
app.include_router(files_router)
app.include_router(gateway_router)
app.include_router(git_router)
app.include_router(images_router)
app.include_router(memory_router)
app.include_router(proxy_router)
app.include_router(search_router)
app.include_router(skills_router)
Expand Down
40 changes: 40 additions & 0 deletions cptr/frontend/src/lib/apis/memory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { fetchJSON, jsonBody } from '$lib/apis';

export type MemoryScope = 'user' | 'workspace';
export type MemoryOperation = {
action: 'add' | 'replace' | 'remove';
content?: string;
old_text?: string;
};

export type MemorySettings = {
enabled: boolean;
tool_enabled: boolean;
background_review_enabled: boolean;
review_interval_turns: number;
user_char_limit: number;
workspace_char_limit: number;
};

export type MemoryState = {
settings: MemorySettings;
user: { entries: string[]; usage: string; path: string };
workspace: { entries: string[]; usage: string; path: string };
};

export const getMemory = (workspace: string) =>
fetchJSON<MemoryState>(`/api/memory?workspace=${encodeURIComponent(workspace || '')}`);

export const updateMemorySettings = (settings: Partial<MemorySettings>) =>
fetchJSON<{ settings: MemorySettings }>('/api/memory/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings })
});

export const updateMemory = (
scope: MemoryScope,
workspace: string,
operations: MemoryOperation[]
) =>
fetchJSON('/api/memory/update', jsonBody({ scope, workspace, operations }));
4 changes: 3 additions & 1 deletion cptr/frontend/src/lib/apis/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export interface ChatSearchResult {
workspace: string;
updated_at: number;
created_at: number;
match_type: 'title' | 'message';
match_type: 'id' | 'title' | 'summary' | 'message' | 'recent';
snippet: string | null;
matched_message_id?: string | null;
matched_role?: string | null;
}

export interface FileSearchResult {
Expand Down
235 changes: 235 additions & 0 deletions cptr/frontend/src/lib/components/Admin/Images.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<script lang="ts">
import { onMount } from 'svelte';
import { toast } from 'svelte-sonner';
import ToggleSwitch from '../common/ToggleSwitch.svelte';
import Spinner from '../common/Spinner.svelte';
import { getAdminConfig, updateConfig } from '$lib/apis/admin';
import { t } from '$lib/i18n';

let loading = $state(true);
let saving = $state(false);

let generationEnabled = $state(false);
let generationBaseUrl = $state('https://api.openai.com/v1');
let generationApiKey = $state('');
let generationModel = $state('gpt-image-1');
let generationSize = $state('');
let hasGenerationKey = $state(false);

let editEnabled = $state(false);
let editBaseUrl = $state('https://api.openai.com/v1');
let editApiKey = $state('');
let editModel = $state('gpt-image-1');
let editSize = $state('');
let hasEditKey = $state(false);

onMount(async () => {
try {
const config = await getAdminConfig();
generationEnabled = config['images.generation_enabled'] === true;
generationBaseUrl =
(config['images.generation_base_url'] as string) || 'https://api.openai.com/v1';
generationModel = (config['images.generation_model'] as string) || 'gpt-image-1';
generationSize = (config['images.generation_size'] as string) || '';
hasGenerationKey = !!config['images.generation_api_key'];

editEnabled = config['images.edit_enabled'] === true;
editBaseUrl = (config['images.edit_base_url'] as string) || 'https://api.openai.com/v1';
editModel = (config['images.edit_model'] as string) || 'gpt-image-1';
editSize = (config['images.edit_size'] as string) || '';
hasEditKey = !!config['images.edit_api_key'];
} catch {}
loading = false;
});

async function save() {
saving = true;
try {
const cfg: Record<string, unknown> = {
'images.generation_enabled': generationEnabled,
'images.generation_base_url': generationBaseUrl,
'images.generation_model': generationModel,
'images.generation_size': generationSize,
'images.edit_enabled': editEnabled,
'images.edit_base_url': editBaseUrl,
'images.edit_model': editModel,
'images.edit_size': editSize
};
if (generationApiKey) {
cfg['images.generation_api_key'] = generationApiKey;
}
if (editApiKey) {
cfg['images.edit_api_key'] = editApiKey;
}
await updateConfig(cfg);
if (generationApiKey) hasGenerationKey = true;
if (editApiKey) hasEditKey = true;
generationApiKey = '';
editApiKey = '';
toast.success($t('settings.saved'));
} catch {
toast.error($t('admin.images.saveFailed'));
} 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">
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
{$t('admin.images.title')}
</h2>

<h3 class="text-xs text-gray-400 dark:text-gray-600 mb-2">
{$t('admin.images.generation')}
</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.images.enableGeneration')}</span
>
<ToggleSwitch
value={generationEnabled}
onchange={(v) => {
generationEnabled = v;
}}
/>
</label>
<p class="text-[11px] text-gray-400 dark:text-gray-600 -mt-1">
{$t('admin.images.generationHint')}
</p>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="image-generation-base-url"
>{$t('connections.baseUrl')}</label
>
<input
id="image-generation-base-url"
type="text"
bind:value={generationBaseUrl}
placeholder="https://api.openai.com/v1"
class="w-full mt-1 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"
/>
</div>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="image-generation-api-key"
>{$t('connections.apiKey')}</label
>
<input
id="image-generation-api-key"
type="password"
bind:value={generationApiKey}
placeholder={hasGenerationKey ? '••••••••' : 'sk-...'}
class="w-full mt-1 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"
/>
</div>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="image-generation-model"
>{$t('automations.model')}</label
>
<input
id="image-generation-model"
type="text"
bind:value={generationModel}
placeholder="gpt-image-1"
class="w-full mt-1 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"
/>
</div>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="image-generation-size"
>{$t('admin.images.size')}</label
>
<input
id="image-generation-size"
type="text"
bind:value={generationSize}
placeholder="1024x1024"
class="w-full mt-1 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"
/>
</div>
</div>

<h3 class="text-xs text-gray-400 dark:text-gray-600 mb-2 mt-5">
{$t('admin.images.editing')}
</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.images.enableEditing')}</span
>
<ToggleSwitch
value={editEnabled}
onchange={(v) => {
editEnabled = v;
}}
/>
</label>
<p class="text-[11px] text-gray-400 dark:text-gray-600 -mt-1">
{$t('admin.images.editHint')}
</p>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="image-edit-base-url"
>{$t('connections.baseUrl')}</label
>
<input
id="image-edit-base-url"
type="text"
bind:value={editBaseUrl}
placeholder="https://api.openai.com/v1"
class="w-full mt-1 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"
/>
</div>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="image-edit-api-key"
>{$t('connections.apiKey')}</label
>
<input
id="image-edit-api-key"
type="password"
bind:value={editApiKey}
placeholder={hasEditKey ? '••••••••' : $t('admin.images.editKeyPlaceholder')}
class="w-full mt-1 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"
/>
</div>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="image-edit-model"
>{$t('automations.model')}</label
>
<input
id="image-edit-model"
type="text"
bind:value={editModel}
placeholder="gpt-image-1"
class="w-full mt-1 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"
/>
</div>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="image-edit-size"
>{$t('admin.images.size')}</label
>
<input
id="image-edit-size"
type="text"
bind:value={editSize}
placeholder="1024x1024"
class="w-full mt-1 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"
/>
</div>
</div>
</div>

<!-- Save -->
<div class="shrink-0 pt-3 flex justify-end">
<button
class="text-[13px] text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors duration-100 disabled:opacity-50"
onclick={() => save()}
disabled={saving}
>
{#if saving}{$t('settings.saving')}{:else}{$t('settings.save')}{/if}
</button>
</div>
{/if}
</div>
2 changes: 1 addition & 1 deletion cptr/frontend/src/lib/components/Admin/Models.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
{ name: 'MODEL', desc: 'Model ID being used' }
];

const DEFAULT_PROMPT_PLACEHOLDER = `You are cptr, a helpful assistant running inside the user's computer interface. You have access to tools to read, search, and modify files in the workspace, run commands, and use configured tools. Use them to help the user directly.
const DEFAULT_PROMPT_PLACEHOLDER = `You are Computer (cptr), a helpful assistant running inside the user's computer interface. You have access to tools to read, search, and modify files in the workspace, run commands, and use configured tools. Use them to help the user directly. Approach hard requests with initiative and persistence: make the best possible attempt, adapt as needed, and keep going unless a real constraint prevents progress.

{{CPTR_CONTEXT}}

Expand Down
Loading
Loading