From d4496a35f73ee5a355b372e3083d5ee8f27faa7e Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Wed, 17 Jun 2026 04:13:42 +0530 Subject: [PATCH 1/5] fix: bump __version__ to 0.22.0 to match release gently/__init__.py still carried 0.22.0.dev0 while pyproject.toml and the README were bumped to 0.22.0. Align it with the release version. Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gently/__init__.py b/gently/__init__.py index 29b0b3b4..25c8c925 100644 --- a/gently/__init__.py +++ b/gently/__init__.py @@ -81,7 +81,7 @@ except ImportError: _VISUALIZATION_AVAILABLE = False -__version__ = "0.22.0.dev0" +__version__ = "0.22.0" __all__ = [ # Main entry point "Gently", From b26c6bbed30729dea5185ce93144e66e00de657b Mon Sep 17 00:00:00 2001 From: Magdalena Schneider Date: Wed, 17 Jun 2026 12:58:12 -0400 Subject: [PATCH 2/5] Fix viz server falsely reporting port in use after clean shutdown --- gently/ui/web/server.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gently/ui/web/server.py b/gently/ui/web/server.py index c16c548d..6107689c 100644 --- a/gently/ui/web/server.py +++ b/gently/ui/web/server.py @@ -784,6 +784,12 @@ async def on_start(self): import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # Match uvicorn's own bind (it sets SO_REUSEADDR): without this, the + # pre-flight check is stricter than the real server and falsely reports + # "port in use" for ~60s after a clean shutdown, while sockets from + # closed browser WebSockets linger in TIME_WAIT. uvicorn would rebind + # fine — so this check must too, and only trip on a live LISTEN holder. + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((self.host, self.port)) except OSError: From 490a8598ed8e39bad6ef42b450800780e8f3ab04 Mon Sep 17 00:00:00 2001 From: Magdalena Schneider Date: Wed, 17 Jun 2026 12:59:31 -0400 Subject: [PATCH 3/5] Show a working indicator while the agent is thinking --- gently/harness/bridge.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gently/harness/bridge.py b/gently/harness/bridge.py index ddbfa9ca..ae244987 100644 --- a/gently/harness/bridge.py +++ b/gently/harness/bridge.py @@ -663,6 +663,13 @@ async def stream_response( pending_choice_result = None try: + # Open the stream envelope immediately — before the (potentially + # slow) context build + Claude API call with extended thinking — so + # every client shows the "Working…" trust signal the instant the + # turn starts, not only once the first token arrives. Pairs with the + # stream_end emitted on StopAsyncIteration below. + await send_fn({"type": "stream_start"}) + while True: try: if pending_choice_result is not None: From 79c7af55abc321bde142fffe91bc6c1a73208026 Mon Sep 17 00:00:00 2001 From: Magdalena Schneider Date: Wed, 17 Jun 2026 13:00:13 -0400 Subject: [PATCH 4/5] Redesign agent chat as a docked collapsible sidebar --- gently/ui/web/static/css/agent-chat.css | 93 +++++++++++--------- gently/ui/web/static/js/agent-chat.js | 46 ++++------ gently/ui/web/static/js/projection-viewer.js | 2 +- gently/ui/web/templates/_header.html | 13 +-- gently/ui/web/templates/index.html | 28 ++++-- 5 files changed, 91 insertions(+), 91 deletions(-) diff --git a/gently/ui/web/static/css/agent-chat.css b/gently/ui/web/static/css/agent-chat.css index fdaaa8e3..b6f02119 100644 --- a/gently/ui/web/static/css/agent-chat.css +++ b/gently/ui/web/static/css/agent-chat.css @@ -1,40 +1,55 @@ /* Floating agent-chat window — the web-side control surface. Restrained, professional styling for a lab instrument. */ -/* ── Header toggle (replaces the floating FAB) ─────────────── */ -.header-agent-toggle { - display: inline-flex; align-items: center; gap: 7px; - padding: 5px 10px; border-radius: 8px; - border: 1px solid var(--border); - background: var(--bg-hover); color: var(--text); - font: 500 12.5px/1 'Inter Tight', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - cursor: pointer; position: relative; - transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease; -} -.header-agent-toggle:hover { border-color: var(--accent); } -.header-agent-toggle[aria-pressed="true"] { - border-color: var(--accent); color: var(--accent); - background: rgba(96, 165, 250, 0.12); -} -.header-agent-toggle svg { display: block; } -.header-agent-label { letter-spacing: 0.01em; } -.header-agent-dot { - width: 7px; height: 7px; border-radius: 50%; - background: var(--text-muted); flex: 0 0 auto; +/* ── Collapsed rail (always-present agent affordance) ────────── + When the docked panel is collapsed it shrinks to this thin vertical rail: + a spark glyph (click to open), a vertical "Agent" label, plus the connection + dot + unseen-activity badge so a tucked-away agent can still signal it woke + or has something pending. Shown only via the body.chat-docked rules below. */ +.agent-rail { + display: none; /* shown only when collapsed (see :not(.open) rule) */ + flex-direction: column; align-items: center; + width: 100%; height: 100%; padding: 8px 0 0; + border: none; background: transparent; + cursor: pointer; } -.header-agent-dot.ok { background: var(--accent-green); } -.header-agent-badge { - position: absolute; top: -6px; right: -6px; +/* Icon-only activity-bar button: contained rounded hover target (VS Code/Google + style), with the connection state + activity count attached to the icon. */ +.agent-rail-icon { + position: relative; + display: flex; align-items: center; justify-content: center; + width: 40px; height: 40px; border-radius: 10px; + color: var(--text-muted); + transition: background 0.15s ease, color 0.15s ease; +} +.agent-rail:hover .agent-rail-icon { background: var(--bg-hover); color: var(--text); } +.agent-rail-spark { display: flex; } +/* Presence dot in the icon's corner (avatar-with-status pattern). */ +.agent-rail-dot { + position: absolute; right: 5px; bottom: 5px; + width: 8px; height: 8px; border-radius: 50%; + background: var(--text-muted); + border: 2px solid var(--bg-card); box-sizing: content-box; +} +.agent-rail-dot.ok { background: var(--accent-green); } +/* Unseen-activity count badge in the icon's top-right corner. */ +.agent-rail-badge { + position: absolute; top: -4px; right: -4px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--accent-purple); color: #fff; font-size: 10px; font-weight: 700; line-height: 16px; text-align: center; + border: 2px solid var(--bg-card); box-sizing: content-box; } -.header-agent-badge.hidden { display: none; } +.agent-rail-badge.hidden { display: none; } /* ── Docked agent panel ──────────────────────────────────── - Default = overlay slide-over, absolutely positioned inside .app-shell (which - sits below the global header/navbar). Pin (body.chat-docked) turns it into a - real column that pushes .app-main. */ + The panel is always docked (body.chat-docked, set on load): a real column + that pushes .app-main, never a float over content. Collapsing it (header + Agent toggle / Ctrl+J / ×) drops it to width 0 to reclaim space. + + The base .agent-chat rules below (absolute, translateX slide) are the + pre-JS / no-chat-docked fallback so the panel stays parked off-screen until + restorePrefs() docks it; body.chat-docked overrides them into the column. */ .agent-chat { position: absolute; top: 0; right: 0; bottom: 0; @@ -54,7 +69,7 @@ } .agent-chat.open { transform: translateX(0); } -/* Pinned: a real pushing column — no float shadow, just a seam. */ +/* Docked: a real pushing column — no float shadow, just a seam. */ body.chat-docked .agent-chat { position: relative; transform: none; @@ -64,9 +79,12 @@ body.chat-docked .agent-chat { transition: none; z-index: auto; } +/* Collapsed: shrink to the rail. Hide every child, then re-show only the rail. */ body.chat-docked .agent-chat:not(.open) { - width: 0; border-left: none; overflow: hidden; + width: 48px; flex: 0 0 48px; overflow: hidden; } +body.chat-docked .agent-chat:not(.open) > * { display: none; } +body.chat-docked .agent-chat:not(.open) > .agent-rail { display: flex; } @media (prefers-reduced-motion: reduce) { .agent-chat { transition: opacity 0.12s ease; } @@ -85,15 +103,6 @@ body.chat-docked .agent-chat:not(.open) { .agent-control-banner.hidden { display: none; } -/* Pin button in the panel header. */ -.agent-chat-pin { - background: none; border: none; color: var(--text-muted); - cursor: pointer; padding: 2px; display: flex; align-items: center; - border-radius: 5px; -} -.agent-chat-pin:hover { color: var(--text); background: var(--bg-hover); } -.agent-chat-pin[aria-pressed="true"] { color: var(--accent); } - /* ── Header ─────────────────────────────────────────────── */ .agent-chat-header { display: flex; @@ -132,10 +141,12 @@ body.chat-docked .agent-chat:not(.open) { .agent-chat-conn.ac-conn-bad { color: var(--accent-orange, #fb923c); border-color: rgba(251, 146, 60, 0.35); } .agent-chat-close { background: none; border: none; - color: var(--text-muted); font-size: 20px; line-height: 1; - cursor: pointer; padding: 0 2px; + color: var(--text-muted); line-height: 1; + cursor: pointer; padding: 2px; border-radius: 5px; + display: inline-flex; align-items: center; } -.agent-chat-close:hover { color: var(--text); } +.agent-chat-close svg { display: block; } +.agent-chat-close:hover { color: var(--text); background: var(--bg-hover); } /* ── Control banner ─────────────────────────────────────── */ .agent-control-banner { diff --git a/gently/ui/web/static/js/agent-chat.js b/gently/ui/web/static/js/agent-chat.js index 63339b92..dda87ce1 100644 --- a/gently/ui/web/static/js/agent-chat.js +++ b/gently/ui/web/static/js/agent-chat.js @@ -37,7 +37,7 @@ const AgentChat = (() => { // DOM refs (resolved in init) let panel, log, input, sendBtn, conn, banner, closeBtn, userEl, signoutBtn; - let toggleBtn, pinBtn, resizeEl, toggleDot, toggleBadge; // docked-panel chrome + let railBtn, resizeEl, toggleDot, toggleBadge; // docked-panel chrome let pendingSlot = null; // sticky slot for ASK approval proposals let acComplete = null; // the autocomplete dropdown element @@ -701,10 +701,9 @@ const AgentChat = (() => { function togglePanel(open) { panelOpen = (open === undefined) ? !panelOpen : open; panel.classList.toggle('open', panelOpen); - if (toggleBtn) { - toggleBtn.setAttribute('aria-pressed', panelOpen ? 'true' : 'false'); - toggleBtn.setAttribute('aria-expanded', panelOpen ? 'true' : 'false'); - } + // Remember collapse state so a reload restores it (defaults to open). + try { localStorage.setItem('gently-chat-open', panelOpen ? '1' : '0'); } catch (_) {} + if (railBtn) railBtn.setAttribute('aria-expanded', panelOpen ? 'true' : 'false'); if (panelOpen) { clearBadge(); if (!ws) connect(); @@ -740,24 +739,6 @@ const AgentChat = (() => { return w; } - function applyDock(docked, persist) { - document.body.classList.toggle('chat-docked', docked); - if (pinBtn) { - pinBtn.setAttribute('aria-pressed', docked ? 'true' : 'false'); - pinBtn.title = docked ? 'Unpin (float over content)' : 'Pin to dock'; - } - if (persist) { try { localStorage.setItem('gently-chat-docked', docked ? '1' : '0'); } catch (_) {} } - // Suppress the slide animation across the mode flip, then notify viewers. - panel.style.transition = 'none'; - requestAnimationFrame(() => { panel.style.transition = ''; emitLayoutChanged(); }); - } - - function togglePin() { - const docked = !document.body.classList.contains('chat-docked'); - if (docked && !panelOpen) togglePanel(true); // pinning implies showing - applyDock(docked, true); - } - function setupResize() { if (!resizeEl) return; let startX = 0, startW = 0, dragging = false, rafId = 0, pid = null; @@ -806,8 +787,14 @@ const AgentChat = (() => { try { const w = parseInt(localStorage.getItem('gently-chat-w')); if (w) setChatWidth(w, false); - if (localStorage.getItem('gently-chat-docked') === '1') applyDock(true, false); } catch (_) {} + // The agent panel is always docked — a real column that pushes content, + // not a float over it. It's open by default; the header Agent toggle / + // Ctrl+J / × collapse it to width 0 to reclaim space for the viewer. + document.body.classList.add('chat-docked'); + let open = true; + try { open = localStorage.getItem('gently-chat-open') !== '0'; } catch (_) {} + togglePanel(open); } // Unseen-activity badge on the header toggle — so a closed panel still tells @@ -863,17 +850,16 @@ const AgentChat = (() => { closeBtn = document.getElementById('agent-chat-close'); userEl = document.getElementById('agent-chat-user'); signoutBtn = document.getElementById('agent-chat-signout'); - toggleBtn = document.getElementById('agent-chat-toggle'); - pinBtn = document.getElementById('agent-chat-pin'); + railBtn = document.getElementById('agent-rail-toggle'); // collapsed-rail spark resizeEl = document.getElementById('agent-chat-resize'); - toggleDot = document.getElementById('agent-chat-toggle-dot'); - toggleBadge = document.getElementById('agent-chat-toggle-badge'); + // Connection dot + unseen-activity badge now live on the collapsed rail. + toggleDot = document.getElementById('agent-rail-dot'); + toggleBadge = document.getElementById('agent-rail-badge'); if (!panel) return; // markup not present restorePrefs(); - if (toggleBtn) toggleBtn.addEventListener('click', () => togglePanel()); + if (railBtn) railBtn.addEventListener('click', () => togglePanel(true)); closeBtn.addEventListener('click', () => togglePanel(false)); - if (pinBtn) pinBtn.addEventListener('click', togglePin); setupResize(); // Ctrl/Cmd+J toggles the panel from anywhere. document.addEventListener('keydown', (e) => { diff --git a/gently/ui/web/static/js/projection-viewer.js b/gently/ui/web/static/js/projection-viewer.js index 0e20b9fa..5cf7719d 100644 --- a/gently/ui/web/static/js/projection-viewer.js +++ b/gently/ui/web/static/js/projection-viewer.js @@ -344,7 +344,7 @@ const ProjectionViewer = { // panel can dock/resize and the window can resize. The animation loop // re-renders every frame, so on a size change we only need to resize the // renderer + camera (coalesced to one rAF). Also listen for the explicit - // layout-change event the chat dock fires on pin/unpin. + // layout-change event the chat dock fires on collapse/expand + resize. if (this._resizeObserver) this._resizeObserver.disconnect(); this._resizeObserver = new ResizeObserver(() => { if (this._resizeRaf) cancelAnimationFrame(this._resizeRaf); diff --git a/gently/ui/web/templates/_header.html b/gently/ui/web/templates/_header.html index 3f113590..cf4a4d03 100644 --- a/gently/ui/web/templates/_header.html +++ b/gently/ui/web/templates/_header.html @@ -25,17 +25,8 @@
- - + {# Agent lives in the docked right sidebar (collapses to a spark rail) — + no header toggle. Ctrl/Cmd+J still opens it. #} {% endif %} diff --git a/gently/ui/web/templates/index.html b/gently/ui/web/templates/index.html index 85a60345..bd36b2c4 100644 --- a/gently/ui/web/templates/index.html +++ b/gently/ui/web/templates/index.html @@ -19,8 +19,8 @@ {% include '_header.html' %} {% include '_navbar.html' %} - +
@@ -587,9 +587,22 @@

Properties

- +