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
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,28 @@ 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.12] - 2026-07-21

### Added

- 📱 **Unsaved file edits now get a proper warning.** Closing an edited file tab now shows Computer's own confirmation dialog, so mobile users and desktop users can choose to keep editing or close without saving.
- 🎛️ **Browser streaming now has an encoder setting.** Admins can leave streaming on Auto, force hardware encoding, or force software encoding from Admin > Web > Streaming quality > Advanced.

### Changed

- 🌍 **More of Computer is translated.** Admin screens, chat controls, diff settings, file editor labels, skill controls, and planning prompts now use the same language as the rest of the app across all supported languages.
- 📜 **The license is clearer for hosted use.** The old multi-tenant hosting restriction was removed while the attribution requirements remain in place.

### Fixed

- 🌐 **Browser tabs work on servers without video hardware.** Streaming now defaults to Auto, so headless servers, virtual machines, and other GPU-light hosts can fall back to software encoding instead of failing to open the browser stream.
- 🤖 **Claude Code appears when it comes from the Claude desktop app.** If Claude Code was installed through Claude desktop on macOS or Windows, Computer can now find it automatically and show it as an available coding agent.

## [0.9.11] - 2026-07-19

### Added

- 🤖 **Gemini is now its own coding agent.** Choose Gemini as an agent and Computer will start the Gemini CLI with its Google sign-in flow, keep the chat resumable, and handle image attachments like the other coding agents.
- 🤖 **Gemini is now its own coding agent.** Choose Gemini as an agent and Computer will start it with its Google sign-in flow, keep the chat resumable, and handle image attachments like the other coding agents.

### Fixed

Expand Down
5 changes: 0 additions & 5 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ Attribution elements are any elements identifying the origin of the software,
such as logos, icons, visual marks, the product name, copyright notices,
attribution statements, and about screens or version information.

Multi-Tenant Use

You may not host, deploy, manage, or otherwise facilitate multiple instances
of the software or its derivative works as a service.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
Expand Down
9 changes: 5 additions & 4 deletions cptr/frontend/src/lib/components/Admin/CreateBotModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
try {
verifyResult = await verifyBotToken(platform, token.trim());
} catch (e: any) {
verifyResult = { ok: false, error: e.message || 'Verification failed' };
verifyResult = { ok: false, error: e.message || $t('messaging.verificationFailed') };
} finally {
verifying = false;
}
Expand Down Expand Up @@ -149,7 +149,7 @@
<input
type="text"
bind:value={name}
placeholder="My Bot"
placeholder={$t('messaging.namePlaceholder')}
autofocus
autocomplete="off"
spellcheck="false"
Expand Down Expand Up @@ -181,7 +181,7 @@
<input
type="text"
bind:value={name}
placeholder="My Bot"
placeholder={$t('messaging.namePlaceholder')}
autofocus
autocomplete="off"
spellcheck="false"
Expand All @@ -191,7 +191,8 @@

<!-- Token -->
<label class="text-[0.625rem] text-gray-400 dark:text-gray-600">
Token {#if bot}<span class="text-gray-300 dark:text-gray-700">({bot.token_masked})</span>{/if}
{$t('messaging.token')}
{#if bot}<span class="text-gray-300 dark:text-gray-700">({bot.token_masked})</span>{/if}
</label>
<div class="flex gap-2 items-center mb-0.5">
<input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,14 @@
for="create-provider-type"
class="text-[0.625rem] text-gray-400 dark:text-gray-600 mt-2"
>
Provider Type
{$t('connections.providerType')}
</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="default">{$t('general.default')}</option>
<option value="llama.cpp">llama.cpp</option>
</select>
{/if}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,15 @@
<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 for="edit-provider-type" class="text-[0.625rem] text-gray-400 dark:text-gray-600 mt-2">
{$t('connections.providerType')}
</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="default">{$t('general.default')}</option>
<option value="llama.cpp">llama.cpp</option>
</select>
{/if}
Expand Down
31 changes: 20 additions & 11 deletions cptr/frontend/src/lib/components/Admin/Skills.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { toast } from 'svelte-sonner';
import { getAdminConfig, updateConfig } from '$lib/apis/admin';
import { t } from '$lib/i18n';
import ToggleSwitch from '$lib/components/common/ToggleSwitch.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';

Expand All @@ -23,7 +24,7 @@
config['skills.background_review_enabled'] !== 'false';
reviewInterval = Number(config['skills.review_interval_turns']) || 10;
} catch {
toast.error('Failed to load config');
toast.error($t('admin.failedToLoadConfig'));
}
loading = false;
});
Expand All @@ -37,9 +38,9 @@
'skills.background_review_enabled': backgroundReview,
'skills.review_interval_turns': Math.max(1, Number(reviewInterval) || 10)
});
toast.success('Saved');
toast.success($t('settings.saved'));
} catch {
toast.error('Failed to save');
toast.error($t('admin.failedToSave'));
} finally {
saving = false;
}
Expand All @@ -51,12 +52,16 @@
<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">Skills</h2>
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
{$t('chat.skills')}
</h2>

<h3 class="text-xs text-gray-400 dark:text-gray-600 mb-2">Behavior</h3>
<h3 class="text-xs text-gray-400 dark:text-gray-600 mb-2">
{$t('admin.skillsBehavior')}
</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">Enable skills</span>
<span class="text-xs text-gray-600 dark:text-gray-400">{$t('admin.skillsEnable')}</span>
<ToggleSwitch
value={enabled}
onchange={(v) => {
Expand All @@ -68,7 +73,7 @@
{#if enabled}
<label class="flex items-center justify-between cursor-pointer">
<span class="text-xs text-gray-600 dark:text-gray-400"
>Assistant can manage skills</span
>{$t('admin.skillsAssistantCanManage')}</span
>
<ToggleSwitch
value={toolEnabled}
Expand All @@ -79,7 +84,9 @@
</label>

<label class="flex items-center justify-between cursor-pointer">
<span class="text-xs text-gray-600 dark:text-gray-400">Background review</span>
<span class="text-xs text-gray-600 dark:text-gray-400"
>{$t('admin.skillsBackgroundReview')}</span
>
<ToggleSwitch
value={backgroundReview}
onchange={(v) => {
Expand All @@ -91,11 +98,13 @@
</div>

{#if enabled}
<h3 class="text-xs text-gray-400 dark:text-gray-600 mb-2 mt-5">Limits</h3>
<h3 class="text-xs text-gray-400 dark:text-gray-600 mb-2 mt-5">
{$t('admin.skillsLimits')}
</h3>
<div class="flex flex-col gap-2.5">
<div>
<label class="text-xs text-gray-600 dark:text-gray-400" for="skills-review-interval">
Review every
{$t('admin.skillsReviewEvery')}
</label>
<input
id="skills-review-interval"
Expand All @@ -115,7 +124,7 @@
disabled={saving}
onclick={save}
>
{saving ? 'Saving...' : 'Save'}
{saving ? $t('settings.saving') : $t('settings.save')}
</button>
</div>
{/if}
Expand Down
4 changes: 3 additions & 1 deletion cptr/frontend/src/lib/components/Admin/Subagents.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@
step="1000"
class="w-20 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">chars</span>
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600"
>{$t('admin.chars')}</span
>
</div>
</div>

Expand Down
40 changes: 39 additions & 1 deletion cptr/frontend/src/lib/components/Admin/Web.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@
let firecrawlBaseUrl = $state('https://api.firecrawl.dev');
let browserUseApiKey = $state('');
let browserUseBaseUrl = $state('https://api.browser-use.com');
type BrowserEncoderHardwareAcceleration = 'no-preference' | 'prefer-hardware' | 'prefer-software';
let browserQualityDefault = $state<'low' | 'balanced' | 'crisp'>('balanced');
let browserEncoderHardwareAcceleration =
$state<BrowserEncoderHardwareAcceleration>('no-preference');
let browserQualityProfiles = $state({
low: { bitrate: 3000000, frame_rate: 15 },
balanced: { bitrate: 6000000, frame_rate: 24 },
Expand Down Expand Up @@ -118,6 +121,14 @@
}
browserQualityMaxResolution = Number(config['browser.quality.max_resolution']) || 1080;
browserQualityMaxBitrate = Number(config['browser.quality.max_bitrate']) || 12000000;
if (
config['browser.encoder.hardware_acceleration'] === 'prefer-hardware' ||
config['browser.encoder.hardware_acceleration'] === 'prefer-software'
) {
browserEncoderHardwareAcceleration = config[
'browser.encoder.hardware_acceleration'
] as BrowserEncoderHardwareAcceleration;
}
} catch {
toast.error($t('admin.failedToLoadConfig'));
}
Expand Down Expand Up @@ -159,6 +170,7 @@
'browser.browser_use_api_key': browserUseApiKey,
'browser.browser_use_base_url': browserUseBaseUrl,
'browser.quality.default': browserQualityDefault,
'browser.encoder.hardware_acceleration': browserEncoderHardwareAcceleration,
'browser.quality.profiles': browserQualityProfiles,
'browser.quality.max_resolution': browserQualityMaxResolution,
'browser.quality.max_bitrate': browserQualityMaxBitrate
Expand Down Expand Up @@ -571,7 +583,9 @@
class="h-7 w-24 rounded-lg border border-gray-200 bg-gray-100 px-2 text-xs text-gray-700 outline-none dark:border-white/8 dark:bg-white/6 dark:text-gray-300"
/>
<datalist id="browser-quality-heights">
<option value="720"></option><option value="1080"></option><option value="1440"></option><option value="2160"></option>
<option value="720"></option><option value="1080"></option><option
value="1440"
></option><option value="2160"></option>
</datalist>
</div>
<div class="flex items-center justify-between gap-4">
Expand All @@ -587,6 +601,30 @@
class="h-7 w-24 rounded-lg border border-gray-200 bg-gray-100 px-2 text-xs text-gray-700 outline-none dark:border-white/8 dark:bg-white/6 dark:text-gray-300"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<span class="text-xs text-gray-600 dark:text-gray-400"
>{$t('admin.browserEncoderHardwareAcceleration')}</span
>
<p class="text-[0.625rem] text-gray-400 dark:text-gray-600">
{$t('admin.browserEncoderHardwareAccelerationHint')}
</p>
</div>
<select
bind:value={browserEncoderHardwareAcceleration}
class="cursor-pointer bg-transparent text-xs text-gray-600 outline-none dark:text-gray-400"
>
<option value="no-preference"
>{$t('admin.browserEncoderAccelerationAuto')}</option
>
<option value="prefer-hardware"
>{$t('admin.browserEncoderAccelerationHardware')}</option
>
<option value="prefer-software"
>{$t('admin.browserEncoderAccelerationSoftware')}</option
>
</select>
</div>
</div>
</Collapsible>
{:else}
Expand Down
3 changes: 2 additions & 1 deletion cptr/frontend/src/lib/components/ChromeBrowser.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { t } from '$lib/i18n';

interface BrowserState {
url: string;
Expand Down Expand Up @@ -652,7 +653,7 @@
<textarea
bind:this={keyboardAntenna}
class="keyboard-antenna"
aria-label="Remote browser keyboard"
aria-label={$t('browser.remoteKeyboard')}
onbeforeinput={beforeInput}
oncompositionstart={() => (composition = true)}
oncompositionupdate={() => {}}
Expand Down
Loading
Loading