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} +
+