diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5faee49..ecb9377a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/cptr/frontend/src/lib/components/FileBrowser.svelte b/cptr/frontend/src/lib/components/FileBrowser.svelte
index 655e85fa..215ea55d 100644
--- a/cptr/frontend/src/lib/components/FileBrowser.svelte
+++ b/cptr/frontend/src/lib/components/FileBrowser.svelte
@@ -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) {
@@ -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) {
diff --git a/cptr/frontend/src/lib/components/Settings/Appearance.svelte b/cptr/frontend/src/lib/components/Settings/Appearance.svelte
index 34af60cb..0fad0393 100644
--- a/cptr/frontend/src/lib/components/Settings/Appearance.svelte
+++ b/cptr/frontend/src/lib/components/Settings/Appearance.svelte
@@ -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,
@@ -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(() => {
@@ -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) {
@@ -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;
@@ -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;
@@ -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);
}
@@ -113,6 +163,7 @@
theme: $theme,
themeConfig: sanitizeThemeConfig($themeConfig),
textScale: $textScale,
+ borderContrast: $borderContrast,
widescreenMode: $widescreenMode
};
@@ -162,11 +213,15 @@
: 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');
@@ -174,6 +229,10 @@
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 {
@@ -275,6 +334,59 @@
/>
+
+
+
+ {$t('appearance.borderContrast')}
+
+
+
+ {#if borderContrastEnabled}
+
+
+ setBorderContrastPreference(borderContrastDraft)}
+ />
+
+
+ {/if}
+
+