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.10] - 2026-07-18

### Added

- 🎚️ **You can make borders easier to see.** Appearance settings now include a border contrast control, so people who found the softer 0.9.9 borders hard to follow can make workspace sections stand out again without changing themes.

### Changed

- 🤖 **Coding-agent chats continue more naturally.** Codex, Claude Code, Cursor, Cline, Grok, and OpenCode now send your latest request into the existing agent chat instead of replaying the whole conversation back at the agent.

### Fixed

- 💬 **Open WebUI messages save even when the model arrives as a list.** Computer now accepts Open WebUI's model format and keeps the chat turn instead of dropping it during save.
- 📁 **Dropping a file onto an open folder uploads it once.** File drops on expanded folders now go only to that folder, not both the folder and the current directory.
- 🎚️ **The border contrast slider is easier to use.** The slider track and handle are more visible in light and dark mode.
- 📦 **Fresh installs avoid a known package mismatch.** Computer now stays within the supported `cryptography` release range instead of picking a newer release that can break installs.

## [0.9.9] - 2026-07-16

### Changed
Expand Down
2 changes: 2 additions & 0 deletions cptr/frontend/src/lib/components/FileBrowser.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,7 @@
if (draggedItem && (targetDir === draggedItem || targetDir.startsWith(draggedItem + '/')))
return;
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';

if (dragOverDir !== targetDir) {
Expand Down Expand Up @@ -821,6 +822,7 @@
// Resolve actual target directory (entry itself if dir, or parent expanded dir)
const targetDir = resolveDropTarget(entry) ?? (entry.type === 'directory' ? entry.path : null);
if (!targetDir) return; // root-level file drop — container handles it
e.stopPropagation();

// Handle file browser item drag (single or multi)
if (draggedItem) {
Expand Down
145 changes: 128 additions & 17 deletions cptr/frontend/src/lib/components/Settings/Appearance.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@
import { toast } from 'svelte-sonner';
import Icon from '../Icon.svelte';
import { t } from '$lib/i18n';
import { expandToolDetails, textScale, theme, themeConfig, widescreenMode } from '$lib/stores';
import {
borderContrast,
expandToolDetails,
textScale,
theme,
themeConfig,
widescreenMode
} from '$lib/stores';
import ToggleSwitch from '$lib/components/common/ToggleSwitch.svelte';
import type { Theme, ThemeConfig } from '$lib/stores';
import {
DEFAULT_BORDER_CONTRAST,
MAX_BORDER_CONTRAST,
normalizeBorderContrast,
normalizeHexColor,
resolveThemeMode,
resolveThemeConfig,
Expand All @@ -14,16 +24,19 @@

const minTextScale = 1;
const maxTextScale = 1.5;
const borderContrastStep = 0.5;

let fileInput: HTMLInputElement;
let scaleEnabled = $state(false);
let scaleDraft = $state(1);
let borderContrastEnabled = $state(false);
let borderContrastDraft = $state(DEFAULT_BORDER_CONTRAST);
let colorDrafts = $state({ background: '', foreground: '' });

const resolvedTheme = $derived(resolveThemeMode($theme));
const resolvedConfig = $derived(resolveThemeConfig($theme, $themeConfig));
const hasCustomAppearance = $derived(
Boolean($themeConfig || $textScale !== null || $widescreenMode)
Boolean($themeConfig || $textScale !== null || $borderContrast !== null || $widescreenMode)
);

$effect(() => {
Expand All @@ -37,6 +50,12 @@
} else if (!scaleEnabled) {
scaleDraft = 1;
}
if ($borderContrast !== null) {
borderContrastEnabled = true;
borderContrastDraft = $borderContrast;
} else if (!borderContrastEnabled) {
borderContrastDraft = DEFAULT_BORDER_CONTRAST;
}
});

function setTheme(v: Theme) {
Expand Down Expand Up @@ -78,6 +97,17 @@
}
}

function toggleBorderContrast() {
if (borderContrastEnabled) {
borderContrastEnabled = false;
borderContrastDraft = DEFAULT_BORDER_CONTRAST;
borderContrast.set(null);
} else {
borderContrastEnabled = true;
borderContrastDraft = $borderContrast ?? DEFAULT_BORDER_CONTRAST;
}
}

function normalizeTextScale(scale: number | string) {
const value = Number(scale);
if (!Number.isFinite(value)) return minTextScale;
Expand All @@ -88,6 +118,11 @@
return `${scale.toFixed(scale % 1 === 0 ? 0 : 2)}x`;
}

function borderContrastLabel(contrast: number | null) {
if (contrast === null) return $t('general.default');
return `${contrast.toFixed(contrast % 1 === 0 ? 0 : 1)}%`;
}

function setTextScalePreference(scale: number | string) {
const next = normalizeTextScale(scale);
scaleDraft = next;
Expand All @@ -100,11 +135,26 @@
}
}

function setBorderContrastPreference(contrast: number | string) {
const next = normalizeBorderContrast(contrast) ?? DEFAULT_BORDER_CONTRAST;
borderContrastDraft = next;
if (next === DEFAULT_BORDER_CONTRAST) {
borderContrastEnabled = false;
borderContrast.set(null);
} else {
borderContrastEnabled = true;
borderContrast.set(next);
}
}

function resetAppearance() {
themeConfig.set(null);
scaleEnabled = false;
scaleDraft = 1;
textScale.set(null);
borderContrastEnabled = false;
borderContrastDraft = DEFAULT_BORDER_CONTRAST;
borderContrast.set(null);
widescreenMode.set(false);
}

Expand All @@ -113,6 +163,7 @@
theme: $theme,
themeConfig: sanitizeThemeConfig($themeConfig),
textScale: $textScale,
borderContrast: $borderContrast,
widescreenMode: $widescreenMode
};

Expand Down Expand Up @@ -162,18 +213,26 @@
: undefined;
const importedWidescreenMode =
typeof parsed?.widescreenMode === 'boolean' ? parsed.widescreenMode : undefined;
const importedBorderContrast =
normalizeBorderContrast(parsed?.borderContrast) ??
(parsed?.highContrastBorders === true ? 12 : undefined);

if (
!importedConfig &&
!importedTheme &&
importedScale === undefined &&
importedBorderContrast === undefined &&
importedWidescreenMode === undefined
) {
throw new Error('empty theme');
}
if (importedTheme) theme.set(importedTheme);
if (importedConfig) themeConfig.set(importedConfig);
if (importedScale !== undefined) textScale.set(importedScale === 1 ? null : importedScale);
if (importedBorderContrast !== undefined)
borderContrast.set(
importedBorderContrast === DEFAULT_BORDER_CONTRAST ? null : importedBorderContrast
);
if (importedWidescreenMode !== undefined) widescreenMode.set(importedWidescreenMode);
toast.success($t('appearance.imported'));
} catch {
Expand Down Expand Up @@ -275,6 +334,59 @@
/>
</label>

<div class="w-full mt-3">
<div class="flex items-center gap-2">
<span id="border-contrast-label" class="text-xs text-gray-600 dark:text-gray-400">
{$t('appearance.borderContrast')}
</span>
<button
type="button"
class="ml-auto h-6 px-2 rounded-lg text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/6 transition-colors"
aria-live="polite"
onclick={toggleBorderContrast}
>
{borderContrastEnabled ? borderContrastLabel(borderContrastDraft) : $t('general.default')}
</button>
</div>
{#if borderContrastEnabled}
<div class="flex items-center gap-1.5 pt-1.5">
<button
type="button"
class="flex items-center justify-center w-6 h-6 rounded-lg text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/6 transition-colors"
aria-labelledby="border-contrast-label"
aria-label={$t('appearance.decreaseBorderContrast')}
onclick={() => setBorderContrastPreference(borderContrastDraft - borderContrastStep)}
>
<Icon name="minus" size={12} />
</button>
<input
id="border-contrast-slider"
class="appearance-range flex-1 min-w-0"
type="range"
min={DEFAULT_BORDER_CONTRAST}
max={MAX_BORDER_CONTRAST}
step={borderContrastStep}
bind:value={borderContrastDraft}
aria-labelledby="border-contrast-label"
aria-valuemin={DEFAULT_BORDER_CONTRAST}
aria-valuemax={MAX_BORDER_CONTRAST}
aria-valuenow={borderContrastDraft}
aria-valuetext={borderContrastLabel(borderContrastDraft)}
oninput={() => setBorderContrastPreference(borderContrastDraft)}
/>
<button
type="button"
class="flex items-center justify-center w-6 h-6 rounded-lg text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/6 transition-colors"
aria-labelledby="border-contrast-label"
aria-label={$t('appearance.increaseBorderContrast')}
onclick={() => setBorderContrastPreference(borderContrastDraft + borderContrastStep)}
>
<Icon name="plus" size={12} />
</button>
</div>
{/if}
</div>

<label class="flex items-center justify-between gap-3 mt-3">
<span class="text-xs text-gray-600 dark:text-gray-400">{$t('appearance.widescreenMode')}</span
>
Expand Down Expand Up @@ -316,7 +428,7 @@
</button>
<input
id="ui-scale-slider"
class="ui-scale-range flex-1 min-w-0"
class="appearance-range flex-1 min-w-0"
type="range"
min={minTextScale}
max={maxTextScale}
Expand Down Expand Up @@ -354,41 +466,40 @@
padding: 0.125rem;
}

.ui-scale-range {
.appearance-range {
appearance: none;
height: 1rem;
background: transparent;
cursor: pointer;
}

.ui-scale-range::-webkit-slider-runnable-track {
height: 0.125rem;
.appearance-range::-webkit-slider-runnable-track {
height: 0.1875rem;
border-radius: 624.9375rem;
background: var(--app-divider);
background: color-mix(in oklab, var(--app-fg) 24%, transparent);
}

.ui-scale-range::-webkit-slider-thumb {
.appearance-range::-webkit-slider-thumb {
appearance: none;
width: 0.75rem;
height: 0.75rem;
margin-top: -0.3125rem;
border-radius: 624.9375rem;
border: 1px solid var(--app-border);
background: var(--app-bg);
border: 1px solid color-mix(in oklab, var(--app-bg) 70%, transparent);
background: color-mix(in oklab, var(--app-fg) 82%, var(--app-bg));
}

.ui-scale-range::-moz-range-track {
height: 0.125rem;
.appearance-range::-moz-range-track {
height: 0.1875rem;
border-radius: 624.9375rem;
background: var(--app-divider);
background: color-mix(in oklab, var(--app-fg) 24%, transparent);
}

.ui-scale-range::-moz-range-thumb {
.appearance-range::-moz-range-thumb {
width: 0.75rem;
height: 0.75rem;
border-radius: 624.9375rem;
border: 1px solid var(--app-border);
background: var(--app-bg);
border: 1px solid color-mix(in oklab, var(--app-bg) 70%, transparent);
background: color-mix(in oklab, var(--app-fg) 82%, var(--app-bg));
}

</style>
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,9 @@
"appearance.background": "Hintergrund",
"appearance.foreground": "Vordergrund",
"appearance.uiFont": "Oberflächenschrift",
"appearance.borderContrast": "Rahmenkontrast",
"appearance.decreaseBorderContrast": "Rahmenkontrast verringern",
"appearance.increaseBorderContrast": "Rahmenkontrast erhöhen",
"appearance.widescreenMode": "Breitbildmodus",
"appearance.expandToolDetails": "Aktivitäten immer erweitern",
"appearance.exported": "Design exportiert",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,9 @@
"appearance.background": "Background",
"appearance.foreground": "Foreground",
"appearance.uiFont": "UI font",
"appearance.borderContrast": "Border contrast",
"appearance.decreaseBorderContrast": "Decrease border contrast",
"appearance.increaseBorderContrast": "Increase border contrast",
"appearance.widescreenMode": "Widescreen Mode",
"appearance.expandToolDetails": "Always Expand Activity",
"appearance.exported": "Theme exported",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,9 @@
"appearance.background": "Fondo",
"appearance.foreground": "Primer plano",
"appearance.uiFont": "Fuente de interfaz",
"appearance.borderContrast": "Contraste de bordes",
"appearance.decreaseBorderContrast": "Disminuir contraste de bordes",
"appearance.increaseBorderContrast": "Aumentar contraste de bordes",
"appearance.widescreenMode": "Modo panorámico",
"appearance.expandToolDetails": "Expandir siempre la actividad",
"appearance.exported": "Tema exportado",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,9 @@
"appearance.background": "Arrière-plan",
"appearance.foreground": "Premier plan",
"appearance.uiFont": "Police de l’interface",
"appearance.borderContrast": "Contraste des bordures",
"appearance.decreaseBorderContrast": "Réduire le contraste des bordures",
"appearance.increaseBorderContrast": "Augmenter le contraste des bordures",
"appearance.widescreenMode": "Mode grand écran",
"appearance.expandToolDetails": "Toujours développer l’activité",
"appearance.exported": "Thème exporté",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,9 @@
"appearance.background": "背景",
"appearance.foreground": "前景",
"appearance.uiFont": "UIフォント",
"appearance.borderContrast": "境界線のコントラスト",
"appearance.decreaseBorderContrast": "境界線のコントラストを下げる",
"appearance.increaseBorderContrast": "境界線のコントラストを上げる",
"appearance.widescreenMode": "ワイドスクリーンモード",
"appearance.expandToolDetails": "アクティビティを常に展開",
"appearance.exported": "テーマをエクスポートしました",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,9 @@
"appearance.background": "배경",
"appearance.foreground": "전경",
"appearance.uiFont": "UI 글꼴",
"appearance.borderContrast": "테두리 대비",
"appearance.decreaseBorderContrast": "테두리 대비 낮추기",
"appearance.increaseBorderContrast": "테두리 대비 높이기",
"appearance.widescreenMode": "와이드스크린 모드",
"appearance.expandToolDetails": "항상 활동 펼치기",
"appearance.exported": "테마를 내보냈습니다",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,9 @@
"appearance.background": "Plano de fundo",
"appearance.foreground": "Primeiro plano",
"appearance.uiFont": "Fonte da interface",
"appearance.borderContrast": "Contraste das bordas",
"appearance.decreaseBorderContrast": "Diminuir contraste das bordas",
"appearance.increaseBorderContrast": "Aumentar contraste das bordas",
"appearance.widescreenMode": "Modo tela ampla",
"appearance.expandToolDetails": "Sempre expandir a atividade",
"appearance.exported": "Tema exportado",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,9 @@
"appearance.background": "Фон",
"appearance.foreground": "Передний план",
"appearance.uiFont": "Шрифт интерфейса",
"appearance.borderContrast": "Контраст границ",
"appearance.decreaseBorderContrast": "Уменьшить контраст границ",
"appearance.increaseBorderContrast": "Увеличить контраст границ",
"appearance.widescreenMode": "Широкоэкранный режим",
"appearance.expandToolDetails": "Всегда разворачивать действия",
"appearance.exported": "Тема экспортирована",
Expand Down
Loading
Loading