Skip to content
Merged

0.9.8 #147

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

### Added

- 🏠 **Choose how `.cptr` is handled in projects.** Admins can now turn the automatic `.gitignore` entry on or off, so local chats, memory, caches, audio samples, and generated files stay private by default unless you choose otherwise.
- 🤖 **A llama.cpp connection mode.** OpenAI-compatible connections can now be marked as llama.cpp, so Qwen and other local reasoning models can receive prior thinking in the format llama.cpp expects.
- 🧩 **Skills can open their own companion files.** `view_skill` can now load files such as `references/guide.md` from the selected skill folder, without opening broader home-directory access.
- 🌐 **Sized browser screenshots.** Browser tasks can request a screenshot at a specific size and Computer will restore the normal page view afterward.

### Changed

- ⚙️ **Admin switches behave like switches.** In Models, Agents, Connections, Messaging, and Tool Servers, click the row text to edit and click the switch only to turn something on or off.
- 🔎 **Workspace search respects `.gitignore`.** File search and content search now skip ignored folders and files, keeping generated output, caches, and local Computer data out of normal results.
- 🧠 **Reasoning replay is stricter by default.** Computer now keeps strict OpenAI-compatible connections clean, and only sends llama.cpp-style `reasoning_content` when the connection is set to llama.cpp.

### Fixed

- 🧠 **The chat size meter no longer freezes behind llama.cpp.** Computer now normalizes token usage from OpenAI-compatible streams and sends live context updates to the chat while a response is still running.
- 💬 **Compacted chats survive regenerated answers better.** Summaries are saved on a stable kept user message when possible, so regenerating an assistant answer is less likely to lose the compacted history.
- ✂️ **Manual compact keeps the latest turn available.** Compacting a chat now summarizes the older branch and leaves the newest message ready for the next response.
- 🧠 **Prior reasoning is preserved more reliably.** Reasoning from ordinary replies, tool-call turns, Chat Completions streams, and Responses streams is less likely to be dropped before the next model request.
- 🎨 **Token usage details match the chosen theme.** The usage popover now uses Computer's app surface colors instead of the wrong gray background.
- ✨ **Messages render more faithfully.** Your own messages can show Markdown, and raw markup appears as text instead of disappearing.
- 🗂️ **Commit suggestions are less brittle.** If the model returns a plain commit message instead of perfect structured output, Computer can still use the useful parts.
- 📝 **File edits keep text encoding consistent.** Created and edited files are written as UTF-8, which avoids surprises with non-ASCII project files.

## [0.9.7] - 2026-07-12

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions cptr/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ def _env_int(name: str, default: int) -> int:
# Claude SDK stdout JSON buffer; chat/tool output caps apply later after parsing.
CLAUDE_CODE_MAX_BUFFER_SIZE = _env_int("CPTR_CLAUDE_CODE_MAX_BUFFER_SIZE", 128 * 1024 * 1024)

# ── Workspace storage ───────────────────────────────────────
WORKSPACE_AUTO_GITIGNORE_DOT_CPTR_ENV = os.environ.get("CPTR_AUTO_GITIGNORE_DOT_CPTR")
WORKSPACE_AUTO_GITIGNORE_DOT_CPTR = _env_bool("CPTR_AUTO_GITIGNORE_DOT_CPTR", "true")

# ── Execute timeout ─────────────────────────────────────────
# Default wait (seconds) for run_command / check_task when the caller
# doesn't pass an explicit wait value. None = return immediately.
Expand Down
4 changes: 2 additions & 2 deletions cptr/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cptr/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,4 @@
"tippy.js": "^6.3.7",
"xlsx": "^0.18.5"
}
}
}
4 changes: 2 additions & 2 deletions cptr/frontend/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
--app-fg: #525252;
--app-fg-muted: color-mix(in oklab, var(--app-fg) 62%, var(--app-bg));
--app-fg-subtle: color-mix(in oklab, var(--app-fg) 48%, var(--app-bg));
--app-border: color-mix(in oklab, var(--app-fg) 7%, transparent);
--app-divider: color-mix(in oklab, var(--app-fg) 5%, transparent);
--app-border: color-mix(in oklab, var(--app-fg) 5%, transparent);
--app-divider: color-mix(in oklab, var(--app-fg) 3.5%, transparent);
--app-hover: color-mix(in oklab, var(--app-fg) 6%, transparent);
--app-active: color-mix(in oklab, var(--app-fg) 7%, transparent);
--app-ui-font: var(--font-sans);
Expand Down
2 changes: 2 additions & 0 deletions cptr/frontend/src/lib/apis/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface Connection {
name: string;
provider: string;
api_type: string;
provider_type?: 'default' | 'llama.cpp' | null;
prefix_id: string | null;
base_url: string | null;
api_key: string | null;
Expand All @@ -146,6 +147,7 @@ export const createConnection = (conn: {
name: string;
provider: string;
api_type?: string;
provider_type?: 'default' | 'llama.cpp' | null;
prefix_id?: string | null;
base_url?: string | null;
api_key?: string | null;
Expand Down
7 changes: 5 additions & 2 deletions cptr/frontend/src/lib/apis/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,11 @@ export const answerAskUser = (
export const cancelTask = (chatId: string, messageId: string) =>
fetchJSON(`/api/chats/${chatId}/messages/${messageId}/cancel`, { method: 'POST' });

export const compactChat = (chatId: string, modelId: string) =>
fetchJSON<CompactChatResult>(`/api/chats/${chatId}/compact`, jsonBody({ model_id: modelId }));
export const compactChat = (chatId: string, modelId?: string | null) =>
fetchJSON<CompactChatResult>(
`/api/chats/${chatId}/compact`,
jsonBody({ model_id: modelId || null })
);

export const updateCurrentMessage = (chatId: string, messageId: string) =>
fetchJSON<{ ok: boolean }>(`/api/chats/${chatId}/current`, jsonBody({ message_id: messageId }));
Expand Down
42 changes: 11 additions & 31 deletions cptr/frontend/src/lib/components/Admin/Agents.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
} from '$lib/apis/admin';
import Icon from '$lib/components/Icon.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import ToggleSwitch from '$lib/components/common/ToggleSwitch.svelte';
import { t } from '$lib/i18n';
import { refreshChatState } from '$lib/stores/chat';
import AgentProfileModal from './AgentProfileModal.svelte';
Expand Down Expand Up @@ -179,8 +180,7 @@
modal = null;
}

function toggleProfile(e: Event, index: number) {
e.stopPropagation();
function toggleProfile(index: number) {
const profile = profiles[index];
if (!profile) return;
const mode = profile.mode === 'disabled' ? 'enabled' : 'disabled';
Expand Down Expand Up @@ -262,11 +262,12 @@
{:else}
<div class="divide-y divide-gray-100 dark:divide-white/5">
{#each profiles as profile, index (profile.id)}
<button
class="group flex items-center gap-2 w-full min-h-9 py-1.5 text-left"
onclick={() => (modal = { mode: 'edit', index, profile })}
>
<span class="flex-1 min-w-0">
<div class="group flex items-center gap-2 w-full min-h-9 py-1.5 text-left">
<button
type="button"
class="flex-1 min-w-0 text-left"
onclick={() => (modal = { mode: 'edit', index, profile })}
>
<span
class="block text-[0.8125rem] truncate {profile.mode === 'disabled'
? 'text-gray-400 dark:text-gray-600'
Expand All @@ -282,36 +283,15 @@
{detected[profile.id]?.message}
</span>
{/if}
</span>
</button>
<span
class="hidden sm:flex items-center gap-1.5 shrink-0 text-[0.6875rem] text-gray-400 dark:text-gray-600"
>
<span class="w-1.5 h-1.5 rounded-full {statusTone(profile)}"></span>
<span>{$t(statusKey(profile))}</span>
</span>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
class="relative w-6 h-3.5 rounded-full shrink-0 cursor-pointer transition-colors duration-150
{profile.mode !== 'disabled' ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}"
role="switch"
tabindex="-1"
aria-checked={profile.mode !== 'disabled'}
onclick={(e) => toggleProfile(e, index)}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleProfile(e, index);
}
}}
>
<span
class="absolute top-0.5 w-2.5 h-2.5 rounded-full transition-all duration-150
{profile.mode !== 'disabled'
? 'left-3 bg-white dark:bg-gray-900'
: 'left-0.5 bg-white dark:bg-gray-500'}"
></span>
</span>
</button>
<ToggleSwitch value={profile.mode !== 'disabled'} onchange={() => toggleProfile(index)} />
</div>
{/each}
</div>

Expand Down
41 changes: 10 additions & 31 deletions cptr/frontend/src/lib/components/Admin/Connections.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { refreshChatState } from '$lib/stores/chat';
import { t } from '$lib/i18n';
import Spinner from '$lib/components/common/Spinner.svelte';
import ToggleSwitch from '$lib/components/common/ToggleSwitch.svelte';

let connections = $state<Connection[]>([]);
let loading = $state(true);
Expand Down Expand Up @@ -35,9 +36,7 @@
load();
}

async function toggleEnabled(e: Event, conn: Connection) {
e.stopPropagation();
const newVal = !conn.enabled;
async function toggleEnabled(conn: Connection, newVal: boolean) {
// Optimistic update
conn.enabled = newVal;
connections = [...connections];
Expand Down Expand Up @@ -72,37 +71,17 @@
{:else}
<div>
{#each connections as conn}
<button
class="group flex items-center gap-2 w-full h-7 text-left"
onclick={() => (editConn = conn)}
>
<span
class="flex-1 text-[0.8125rem] truncate
<div class="group flex items-center gap-2 w-full h-7 text-left">
<button
type="button"
class="flex-1 min-w-0 text-left text-[0.8125rem] truncate
{conn.enabled ? 'text-gray-700 dark:text-gray-300' : 'text-gray-400 dark:text-gray-600'}"
onclick={() => (editConn = conn)}
>
{conn.name}
</span>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
class="relative w-6 h-3.5 rounded-full shrink-0 cursor-pointer transition-colors duration-150
{conn.enabled ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}"
role="switch"
tabindex="-1"
aria-checked={conn.enabled}
onclick={(e) => toggleEnabled(e, conn)}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleEnabled(e, conn);
}
}}
>
<span
class="absolute top-0.5 w-2.5 h-2.5 rounded-full transition-all duration-150
{conn.enabled ? 'left-3 bg-white dark:bg-gray-900' : 'left-0.5 bg-white dark:bg-gray-500'}"
></span>
</span>
</button>
</button>
<ToggleSwitch value={conn.enabled} onchange={(value) => toggleEnabled(conn, value)} />
</div>
{/each}

{#if connections.length === 0}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
let formName = $state('');
let formProvider = $state<'openai' | 'anthropic'>('openai');
let formApiType = $state<'chat_completions' | 'responses'>('chat_completions');
let formProviderType = $state<'default' | 'llama.cpp'>('default');
let formBaseUrl = $state('');
let formApiKey = $state('');
let formPrefixId = $state('');
Expand Down Expand Up @@ -49,6 +50,7 @@
name,
provider: formProvider,
api_type: formProvider === 'openai' ? formApiType : 'chat_completions',
provider_type: formProvider === 'openai' ? formProviderType : 'default',
prefix_id: formPrefixId.trim() || null,
base_url: formBaseUrl.trim(),
api_key: formApiKey.trim(),
Expand Down Expand Up @@ -115,6 +117,21 @@
<option value="chat_completions">{$t('connections.chatCompletions')}</option>
<option value="responses">{$t('connections.responses')}</option>
</select>

<label
for="create-provider-type"
class="text-[0.625rem] text-gray-400 dark:text-gray-600 mt-2"
>
Provider Type
</label>
<select
id="create-provider-type"
bind:value={formProviderType}
class="block w-full bg-transparent text-[0.8125rem] text-gray-700 dark:text-gray-300 outline-none py-0.5 cursor-pointer"
>
<option value="default">Default</option>
<option value="llama.cpp">llama.cpp</option>
</select>
{/if}

<label class="text-[0.625rem] text-gray-400 dark:text-gray-600 mt-2"
Expand Down
19 changes: 19 additions & 0 deletions cptr/frontend/src/lib/components/Admin/EditConnectionModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
let formApiType = $state<'chat_completions' | 'responses'>(
connection.api_type === 'responses' ? 'responses' : 'chat_completions'
);
let formProviderType = $state<'default' | 'llama.cpp'>(
connection.provider_type === 'llama.cpp' ? 'llama.cpp' : 'default'
);
let formBaseUrl = $state(connection.base_url || '');
let formApiKey = $state('');
let formPrefixId = $state(connection.prefix_id || '');
Expand Down Expand Up @@ -61,6 +64,7 @@
name,
provider: formProvider,
api_type: formProvider === 'openai' ? formApiType : 'chat_completions',
provider_type: formProvider === 'openai' ? formProviderType : 'default',
prefix_id: formPrefixId.trim() || null,
base_url: formBaseUrl.trim(),
models
Expand Down Expand Up @@ -151,6 +155,21 @@
<option value="chat_completions">{$t('connections.chatCompletions')}</option>
<option value="responses">{$t('connections.responses')}</option>
</select>

<label
for="edit-provider-type"
class="text-[0.625rem] text-gray-400 dark:text-gray-600 mt-2"
>
Provider Type
</label>
<select
id="edit-provider-type"
bind:value={formProviderType}
class="block w-full bg-transparent text-[0.8125rem] text-gray-700 dark:text-gray-300 outline-none py-0.5 cursor-pointer"
>
<option value="default">Default</option>
<option value="llama.cpp">llama.cpp</option>
</select>
{/if}

<label class="text-[0.625rem] text-gray-400 dark:text-gray-600 mt-2"
Expand Down
26 changes: 3 additions & 23 deletions cptr/frontend/src/lib/components/Admin/Messaging.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { listBots, deleteBot, startBot, stopBot, type BotData } from '$lib/apis/bots';
import { t } from '$lib/i18n';
import Spinner from '$lib/components/common/Spinner.svelte';
import ToggleSwitch from '$lib/components/common/ToggleSwitch.svelte';

let bots = $state<BotData[]>([]);
let loading = $state(true);
Expand All @@ -29,8 +30,7 @@
load();
}

async function toggleRunning(e: Event, bot: BotData) {
e.stopPropagation();
async function toggleRunning(bot: BotData) {
const wasRunning = bot.is_running;
// Optimistic
bot.is_running = !wasRunning;
Expand Down Expand Up @@ -108,27 +108,7 @@
<Icon name="trash" size={11} />
</button>

<!-- Toggle switch -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
class="relative w-6 h-3.5 rounded-full shrink-0 cursor-pointer transition-colors duration-150
{bot.is_running ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}"
role="switch"
tabindex="-1"
aria-checked={bot.is_running}
onclick={(e) => toggleRunning(e, bot)}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleRunning(e, bot);
}
}}
>
<span
class="absolute top-0.5 w-2.5 h-2.5 rounded-full transition-all duration-150
{bot.is_running ? 'left-3 bg-white dark:bg-gray-900' : 'left-0.5 bg-white dark:bg-gray-500'}"
></span>
</span>
<ToggleSwitch value={bot.is_running} onchange={() => toggleRunning(bot)} />
</div>
{/each}

Expand Down
Loading
Loading