Skip to content
Merged
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
152 changes: 152 additions & 0 deletions crates/daemon/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,47 @@
background: linear-gradient(90deg, var(--modeline-bg, #082e18), #04140a 70%);
border-bottom: 1px solid var(--border);
box-shadow: inset 0 -1px 0 rgba(57, 255, 136, 0.16), var(--glow-soft);
/* Lets our pull-to-refresh handler track a vertical drag without the
browser fighting it. Horizontal scroll on the header isn't useful. */
touch-action: pan-y;
}
/* Pull-to-refresh affordance. The page is locked at `overflow: hidden` so
the browser's native swipe-to-refresh never engages on mobile and the
URL bar refresh button is hidden on narrow screens. Custom handler
(see `initPullToRefresh`) tracks a downward drag that *starts on the
header* and grows this overlay; on release past the threshold the page
reloads. Starting the drag on the session view instead means "scroll
up", so we deliberately scope to the header only. */
.pull-indicator {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 0;
z-index: 100;
pointer-events: none;
display: flex;
align-items: flex-end;
justify-content: center;
overflow: hidden;
background: linear-gradient(180deg,
color-mix(in srgb, var(--accent) 28%, transparent), transparent);
color: var(--accent);
font-size: 12px;
letter-spacing: 0.04em;
/* Snaps back when released under threshold. During the drag we add
`.dragging` to suppress the transition so the indicator tracks the
finger 1:1. */
transition: height 0.18s ease-out, background 0.18s ease-out;
}
.pull-indicator.dragging { transition: none; }
.pull-indicator.armed {
background: linear-gradient(180deg,
color-mix(in srgb, var(--accent) 55%, transparent), transparent);
}
.pull-indicator .label {
padding-bottom: 6px;
font-weight: bold;
}
.mini-matrix {
flex: 0 0 auto;
Expand Down Expand Up @@ -5309,9 +5350,120 @@
syncBodyHeightToVisualViewport();
}

/**
* Pull-to-refresh on the header.
*
* The app shell is locked to `html, body { overflow: hidden }` so the
* browser's native swipe-down-to-refresh never engages, and on a narrow
* mobile viewport the URL-bar reload button is hidden, leaving no obvious
* way to refresh. Hijacking page-wide swipes would fight the session
* scroll, so this hooks just the <header> bar: a vertical drag that
* starts on the header grows a banner from the top; releasing past
* `THRESHOLD` reloads the page. Anything starting outside the header
* (chat scroll, list scroll, terminal) keeps its existing behavior.
*
* Differentiation from a tap: we only start tracking once the drag has
* moved more than `SLOP` pixels down, so taps on the hamburger, title,
* or connection indicator still fire normally.
*/
function initPullToRefresh() {
const header = document.querySelector("header");
if (!header) return;
const SLOP = 8;
const THRESHOLD = 70;
const MAX_HEIGHT = 120;
const indicator = document.createElement("div");
indicator.className = "pull-indicator";
const label = document.createElement("div");
label.className = "label";
indicator.appendChild(label);
document.body.appendChild(indicator);

let startY = null;
let dragging = false;
let armed = false;

function setLabel(text) {
if (label.textContent !== text) label.textContent = text;
}
function reset() {
startY = null;
dragging = false;
armed = false;
indicator.classList.remove("dragging", "armed");
indicator.style.height = "0";
}

header.addEventListener("touchstart", (e) => {
if (e.touches.length !== 1) {
reset();
return;
}
startY = e.touches[0].clientY;
dragging = false;
armed = false;
}, { passive: true });

header.addEventListener("touchmove", (e) => {
if (startY === null || e.touches.length !== 1) return;
const dy = e.touches[0].clientY - startY;
if (dy < SLOP) {
// Either an upward drag (uninteresting) or still within tap slop.
if (dragging) reset();
return;
}
if (!dragging) {
dragging = true;
indicator.classList.add("dragging");
}
// The header's `touch-action: pan-y` would otherwise let the browser
// scroll, but body is `overflow: hidden` so there's nothing to scroll;
// preventDefault keeps the document from triggering pull-to-refresh
// chrome and lets us own the gesture cleanly.
e.preventDefault();
const height = Math.min(dy - SLOP, MAX_HEIGHT);
indicator.style.height = `${height}px`;
if (dy >= THRESHOLD) {
if (!armed) {
armed = true;
indicator.classList.add("armed");
}
setLabel("↑ release to refresh");
} else {
if (armed) {
armed = false;
indicator.classList.remove("armed");
}
setLabel("↓ pull down to refresh");
}
}, { passive: false });

header.addEventListener("touchend", () => {
if (!dragging) {
reset();
return;
}
if (armed) {
// Brief visual confirm so the user sees the gesture register before
// the page goes white during reload.
indicator.classList.remove("dragging");
indicator.classList.add("armed");
indicator.style.height = `${MAX_HEIGHT}px`;
setLabel("↻ refreshing…");
setTimeout(() => location.reload(), 200);
return;
}
indicator.classList.remove("dragging");
reset();
}, { passive: true });

header.addEventListener("touchcancel", reset, { passive: true });
}

// --- Boot ---------------------------------------------------------------

initMiniMatrix();
initPullToRefresh();
connect();
</script>
</body>
Expand Down
Loading