diff --git a/FEEDBACK.md b/FEEDBACK.md
index 9580c7a..c73e286 100644
--- a/FEEDBACK.md
+++ b/FEEDBACK.md
@@ -699,3 +699,15 @@
- **Status:** done
- **Completed:** 2026-02-13 — commit `84512b1`
- **Description:** Avatar image shows as broken for some deployments. Bardak's instance (deployed by Pelouse) shows a broken image. Works on Nicolas's instance. Likely the avatar URL configured by Pelouse is invalid or blocked. PinchChat should handle broken avatar images gracefully (fallback to initials or default icon).
+
+## Item #67
+- **Date:** 2026-02-13
+- **Priority:** medium
+- **Status:** pending
+- **Description:** Add an update indicator next to the version number in the UI. When a newer Docker image/release is available on GitHub (compare current version vs latest GitHub release tag), show a visual indicator (badge, dot, or link) so users know they can update.
+
+## Item #68
+- **Date:** 2026-02-13
+- **Priority:** high
+- **Status:** pending
+- **Description:** Cursor desync in textarea STILL present after v1.39.2 fix. The cursor position still gets ahead of where characters actually appear. The previous fix (removing padding/bold from backdrop tokens) was insufficient. Need deeper investigation — likely the HighlightedTextarea backdrop and textarea have mismatched rendering (font metrics, line-height, word-wrap differences, or span wrapping in the backdrop creating different text flow). Consider disabling highlight entirely as test, or ensuring backdrop uses identical character-level rendering with zero extra styling that could affect text width/flow.
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index efaa924..2eb4e38 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -1,10 +1,33 @@
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
-import { X, Sparkles, Search, Pin, Trash2, Columns2, Clock, Bot, MessageSquare, Globe, Zap } from 'lucide-react';
+import { X, Sparkles, Search, Pin, Trash2, Columns2, Clock, Bot, MessageSquare, Globe, Zap, ArrowUpCircle } from 'lucide-react';
import type { Session } from '../types';
import { useT } from '../hooks/useLocale';
import { SessionIcon } from './SessionIcon';
import { sessionDisplayName } from '../lib/sessionName';
import { relativeTime } from '../lib/relativeTime';
+import { useUpdateCheck } from '../hooks/useUpdateCheck';
+
+function VersionBadge() {
+ const update = useUpdateCheck(__APP_VERSION__);
+ if (update.available) {
+ return (
+
+ v{__APP_VERSION__}
+
+ v{update.latestVersion}
+
+ );
+ }
+ return (
+ v{__APP_VERSION__}
+ );
+}
const PINNED_KEY = 'pinchchat-pinned-sessions';
const WIDTH_KEY = 'pinchchat-sidebar-width';
@@ -490,7 +513,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit,
- v{__APP_VERSION__}
+
{/* Resize drag handle */}
({ available: false, latestVersion: null, releaseUrl: null });
+
+ useEffect(() => {
+ let timeout: ReturnType;
+
+ async function check() {
+ try {
+ // Check cache first
+ const cached = localStorage.getItem(CACHE_KEY);
+ if (cached) {
+ const { version, url, ts } = JSON.parse(cached);
+ if (Date.now() - ts < CHECK_INTERVAL) {
+ if (version && isNewer(version, currentVersion)) {
+ setInfo({ available: true, latestVersion: version, releaseUrl: url });
+ }
+ timeout = setTimeout(check, CHECK_INTERVAL - (Date.now() - ts));
+ return;
+ }
+ }
+
+ const res = await fetch(GITHUB_API, { headers: { Accept: 'application/vnd.github.v3+json' } });
+ if (!res.ok) return;
+ const data = await res.json();
+ const tag: string = data.tag_name?.replace(/^v/, '') || '';
+ const url: string = data.html_url || '';
+
+ localStorage.setItem(CACHE_KEY, JSON.stringify({ version: tag, url, ts: Date.now() }));
+
+ if (tag && isNewer(tag, currentVersion)) {
+ setInfo({ available: true, latestVersion: tag, releaseUrl: url });
+ }
+ } catch { /* silent */ }
+ timeout = setTimeout(check, CHECK_INTERVAL);
+ }
+
+ check();
+ return () => clearTimeout(timeout);
+ }, [currentVersion]);
+
+ return info;
+}
+
+/** True if remote is newer than local (semver compare) */
+function isNewer(remote: string, local: string): boolean {
+ const r = remote.split('.').map(Number);
+ const l = local.split('.').map(Number);
+ for (let i = 0; i < 3; i++) {
+ if ((r[i] || 0) > (l[i] || 0)) return true;
+ if ((r[i] || 0) < (l[i] || 0)) return false;
+ }
+ return false;
+}
diff --git a/src/index.css b/src/index.css
index 5c711c0..a947ca8 100644
--- a/src/index.css
+++ b/src/index.css
@@ -212,11 +212,12 @@ html, body {
.ht-backdrop,
.ht-textarea {
- /* Must share identical text layout */
+ /* Must share EXACTLY identical text layout — any difference causes cursor desync */
font-family: inherit;
- font-size: 0.875rem; /* text-sm */
- line-height: 1.25rem;
- padding: 0.75rem 1rem; /* py-3 px-4 */
+ font-size: inherit;
+ line-height: inherit;
+ letter-spacing: inherit;
+ word-spacing: inherit;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
@@ -224,6 +225,7 @@ html, body {
overflow-y: auto;
margin: 0;
box-sizing: border-box;
+ /* padding, border, border-radius all come from the shared className */
}
.ht-backdrop {
@@ -231,9 +233,7 @@ html, body {
inset: 0;
pointer-events: none;
color: var(--pc-text-primary);
- border: 1px solid transparent; /* match textarea border width to keep text aligned */
- border-radius: 1rem; /* rounded-2xl */
- max-height: 200px;
+ background: transparent;
}
.ht-textarea {
@@ -246,38 +246,31 @@ html, body {
z-index: 1;
}
-/* Token colors */
+/* Token colors — ONLY color allowed, nothing that changes text geometry */
.ht-code-block {
color: #67e8f9; /* cyan-300 */
- background: var(--pc-accent-glow);
- border-radius: 4px;
}
.ht-inline-code {
color: #67e8f9;
- background: var(--pc-accent-glow);
- border-radius: 3px;
- /* No padding — must match textarea text layout exactly to avoid cursor desync */
}
.ht-bold {
color: var(--pc-text-primary);
- /* No font-weight change — bold text is wider and causes cursor desync */
}
.ht-italic {
color: var(--pc-text-secondary);
- font-style: italic;
+ /* No font-style: italic — italic glyphs have different widths, causes cursor desync */
}
.ht-heading {
color: #a78bfa; /* violet-400 */
- font-weight: 600;
+ /* No font-weight — bolder glyphs are wider, causes cursor desync */
}
.ht-link {
color: #67e8f9;
- text-decoration: underline;
}
/* Accessibility: respect reduced-motion preferences */