diff --git a/robot-docs/web.md b/robot-docs/web.md index e3c6cc6f..89dd0796 100644 --- a/robot-docs/web.md +++ b/robot-docs/web.md @@ -24,7 +24,7 @@ Returning to the tab (focus or becoming visible) clears the count and closes its Words: a **board** is a named canvas of panels, and the open boards are the tabs along the top (`boards.svelte.ts`). A board holds sessions from any folders. **Workspace** keeps its albedo meaning, a session's folder, and the sidebar shows each session's folder under its name. Its sections are headed as the terminal heads them, the label in the text colour, its count faint, then a thin rule to the edge (the one place lines separate, as `sectionRule` does in the TUI); its rows alternate as the phone's list does, every other row in a section on `--rail-stripe` (the phone's step from its ground, taken from the rail's, about 1.06:1), hover lays a step over a row's own tone, and a session open on the board sits on `--open-tint`, the rail with a flat step of the accent (12% in light, 18% in dark), opaque so the stripes don't show through: a hue over the stripes' tones, under hover's step, and open rows that meet square their corners so a run of them reads as one block. `[` (shown on the sidebar's toggle, as search shows ⌘K; those key chips sit a 12% step of ink above their button) hides or shows the sidebar; hidden, it pops out floating over the canvas, without animation, as soon as the pointer is within 20 px of the canvas's edge on its side, or further out the faster it is heading there (15 px more per px/ms, up to 50 px), so a flick toward the edge opens it early (`App.svelte`, watched from the canvas so the edge still pans). It stays while the pointer is on it or within 48 px of it, the top bar above it included, or while its filter has focus, and goes 100 ms after the pointer is clearly elsewhere or leaves the page; `/` brings it out the same way with the filter focused (Escape clears it and puts it away), and only focuses the filter when the sidebar is shown. `+` or B makes a new board; double-click or F2 renames one; closing a tab (⇧W) keeps the board, which the palette reopens (`Open board: `); `Delete board` removes it. The last tab cannot be closed, so there is always a canvas. -A board is an open canvas. **Windows** sit anywhere on it; the view pans freely and zooms out to an overview. Svelte Flow draws, drags and resizes the windows; `board.svelte.ts` places new ones (right of the selected window, top-aligned, stepping past overlaps) and owns the viewport. The wheel is handled in `Canvas.svelte` rather than by Svelte Flow, because windows and canvas share the trackpad: a pinch (ctrl-wheel) zooms around the pointer anywhere, a mouse wheel's notch with ctrl or ⌘ by one 10% step (a notch is a jump of 50 px or more, or any line-mode delta, where a pinch moves a few pixels at a time), an up-down scroll over a window that can scroll scrolls it while the zoom is readable (60% and up, counting the window's own zoom; scrolled natively; when the browser's scroll stays latched on the canvas, after a pinch over empty canvas or a pan under a still pointer, and a frame passes with room to scroll and no movement, the rest of that gesture moves the window by the fingers' own deltas, with no animation), so does a sideways scroll over anything under the pointer that scrolls sideways (a diff's code inside Pierre's shadow root, a wide table; found through `composedPath()`), a menu floating over the canvas (`MenuButton`'s list) or a view that pans and zooms itself (the agents tree), marked `holds-wheel`, keeps every scroll over it at that zoom even when it has nothing to scroll, and every other scroll pans. Dragging empty canvas pans; holding Space (unless a text field has focus) or the middle button drags the canvas from anywhere, over windows too, taking the pointer before Svelte Flow or the window sees it; holding Alt (Option), a drag anywhere on a window or note moves it, not only by its title bar, without selecting text, and the click that ends it is swallowed (Option-click would download a link), and Space with a double-click anywhere glides the view there, centred at 90% (`Board.centreOn`, 300 ms from the middle of the pane up to 500 ms from a corner), where a plain double-click on empty canvas leaves a note. On a touch screen (`Canvas.svelte`), two fingers anywhere, over windows too, zoom the view around the spot between them and pan it with their travel (Svelte Flow's own pinch stays off, and Safari's page zoom is cancelled); one finger scrolls a window natively or pans empty canvas, and the touches of a pinch go no further until every finger lifts. Under `pointer: coarse` the invisible resize edges reach 14 px and the corners 40 px past a window, and edges, corners and title bars take `touch-action: none` so the browser never claims their drag as a scroll. + and − zoom the canvas in and out by 5 points (the buttons too), ⇧1 fits all windows, ⇧0 returns to 100%, M shows an overview map, ⌥←/⌥→ moves between windows, W closes the selected window, Enter focuses its field (and Escape leaves a chat composer so canvas keys work), S opens the new-session dialog, A opens the artifact gallery, R refreshes the session list, and ? opens the about pane. Each window also keeps its own zoom (`data.zoom` on the board, 50% to 200%, applied to `WindowFrame`'s `.body`): its title bar has a quiet `− +` pill; pointing at it (or focusing it) floats the level beside it on the left, over the chips so nothing moves, with the covered chip fading out under it, and once the window is zoomed the level stays there in the accent and takes it back to 100% when clicked, Alt (Option) with a scroll over a window zooms that window, and ⌥+, ⌥− and ⌥0 step or reset the selected window's zoom. The view controls (zoom, fit, map) in the bottom-right corner, under the overview map when it shows (as wide as the controls, measured, and showing and fading with them), and key hints in the bottom-left as the terminal's footer lists them (spc and a grab to pan, ⌥ and a move to move a window, cmd and a scroll to zoom, ⇧1, ⌥←→, spc and two clicks to zoom at a spot, two clicks for a note; the mouse gestures are Lucide's icons, drawn in `Gesture.svelte`, to keep them short; keys in the text colour, what they do muted, each pair on a surface of its own with a small gap between), fade in while the view moves and fade out 4 s after it stops; hovering or focusing them keeps them, and once faded they let clicks through to the windows beneath. What a window shows is a **view**, each framed by `nodes/WindowFrame.svelte`: a session's chat, or a review of one turn's changes; files, terminals and agent-made pages are meant to follow. A view opened from a window names it as its `parent`, opens beside it, and the canvas draws a faint dashed line between them (the board derives Svelte Flow edges from those parents; every window carries invisible handles for them). +A board is an open canvas. **Windows** sit anywhere on it; the view pans freely and zooms out to an overview. Svelte Flow draws, drags and resizes the windows; `board.svelte.ts` places new ones (right of the selected window, top-aligned, stepping past overlaps) and owns the viewport. The wheel is handled in `Canvas.svelte` rather than by Svelte Flow, because windows and canvas share the trackpad: a pinch (ctrl-wheel) zooms around the pointer anywhere, a mouse wheel's notch with ctrl or ⌘ by one 10% step (a notch is a jump of 50 px or more, or any line-mode delta, where a pinch moves a few pixels at a time), an up-down scroll over a window that can scroll scrolls it while the zoom is readable (60% and up, counting the window's own zoom; scrolled natively; when the browser's scroll stays latched on the canvas, after a pinch over empty canvas or a pan under a still pointer, and a frame passes with room to scroll and no movement, the rest of that gesture moves the window by the fingers' own deltas, with no animation), so does a sideways scroll over anything under the pointer that scrolls sideways (a diff's code inside Pierre's shadow root, a wide table; found through `composedPath()`), a menu floating over the canvas (`MenuButton`'s list) or a view that pans and zooms itself (the agents tree), marked `holds-wheel`, keeps every scroll over it at that zoom even when it has nothing to scroll, and every other scroll pans. Dragging empty canvas pans; holding Space (unless a text field has focus) or the middle button drags the canvas from anywhere, over windows too, taking the pointer before Svelte Flow or the window sees it; holding Alt (Option), a drag anywhere on a window or note moves it, not only by its title bar, without selecting text, and the click that ends it is swallowed (Option-click would download a link), and Space with a double-click anywhere glides the view there, centred at 90% (`Board.centreOn`, 300 ms from the middle of the pane up to 500 ms from a corner), where a plain double-click on empty canvas leaves a note. + and − zoom the canvas in and out by 5 points (the buttons too), ⇧1 fits all windows, ⇧0 returns to 100%, M shows an overview map, ⌥←/⌥→ moves between windows, W closes the selected window, Enter focuses its field (and Escape leaves a chat composer so canvas keys work), S opens the new-session dialog, A opens the artifact gallery, R refreshes the session list, and ? opens the about pane. Each window also keeps its own zoom (`data.zoom` on the board, 50% to 200%, applied to `WindowFrame`'s `.body`): its title bar has a quiet `− +` pill; pointing at it (or focusing it) floats the level beside it on the left, over the chips so nothing moves, with the covered chip fading out under it, and once the window is zoomed the level stays there in the accent and takes it back to 100% when clicked, Alt (Option) with a scroll over a window zooms that window, and ⌥+, ⌥− and ⌥0 step or reset the selected window's zoom. The view controls (zoom, fit, map) in the bottom-right corner, under the overview map when it shows (as wide as the controls, measured, and showing and fading with them), and key hints in the bottom-left as the terminal's footer lists them (spc and a grab to pan, ⌥ and a move to move a window, cmd and a scroll to zoom, ⇧1, ⌥←→, spc and two clicks to zoom at a spot, two clicks for a note; the mouse gestures are Lucide's icons, drawn in `Gesture.svelte`, to keep them short; keys in the text colour, what they do muted, each pair on a surface of its own with a small gap between), fade in while the view moves and fade out 4 s after it stops; hovering or focusing them keeps them, and once faded they let clicks through to the windows beneath. What a window shows is a **view**, each framed by `nodes/WindowFrame.svelte`: a session's chat, or a review of one turn's changes; files, terminals and agent-made pages are meant to follow. A view opened from a window names it as its `parent`, opens beside it, and the canvas draws a faint dashed line between them (the board derives Svelte Flow edges from those parents; every window carries invisible handles for them). The review (`nodes/ReviewNode.svelte`, `review.ts`) opens from "review N files +a −r" on a turn's sign-off, or from the +a −r on any work line, which also scrolls it to the first file that stretch of work changed (`data.jump`). It needs no daemon support beyond history: every tool call's trace records each file it changed, with the unified diff (`changes[].diff`, or `reason` when the file was binary, unreadable or too large). Writes the harness makes for itself stay out of it (`albedo_trace.unobserved()`): the browser plugin's profile lock and marker and its auto-named screenshots (in the cache, see [kernel](kernel.md#browser-gestures)) among them; a screenshot saved to a path the cell chose is still the cell's change. The window keeps the session, the turn and the turn's first history position, reads that turn's entries (`after=`, until the next turn starts), hydrates their traces, and shows the files with their counts, then each file's diffs in order. Diffs are drawn by `@pierre/diffs` (Shiki highlighting, `pierre-dark`/`pierre-light` tokens over the canvas's own ground and diff colours; `views/DiffView.svelte`), loaded with the first review so the rest of the client stays small. The margin button beside a line (drag for several) leaves a note on the canvas beside the window, tied to those lines; "send review" sends every unsent note tied into the review as one message, after an optional overall line. While the session works, the review reads what came after the entries it has every 2.5 s, and once more when it stops (`readTurn` without `seek` ends at the first entry of another turn, so an older turn's review costs one page a read); results already filled in are kept, a file whose changes grew glows in the index for a moment, and the review follows its end as new changes arrive while the reader is at the bottom. diff --git a/web/src/app.css b/web/src/app.css index f6ef4729..e3a46314 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -194,25 +194,6 @@ input:focus-visible, border: 0; } -/* A drag on an edge, a corner or a title bar is always the canvas's, never a scroll. */ -.svelte-flow .svelte-flow__resize-control, -.svelte-flow .window-handle, -.svelte-flow .note-handle { - touch-action: none; -} - -/* A finger needs far more to aim at than a pointer: the edges and corners reach well - past the window, still drawing nothing. */ -@media (pointer: coarse) { - .svelte-flow .window-edge { - border-width: 14px; - } - .svelte-flow .window-corner { - width: 40px; - height: 40px; - } -} - /* Above the view controls in the same corner. */ .svelte-flow__minimap { margin: 14px 14px 60px; diff --git a/web/src/lib/Canvas.svelte b/web/src/lib/Canvas.svelte index 56fb226e..2d69719f 100644 --- a/web/src/lib/Canvas.svelte +++ b/web/src/lib/Canvas.svelte @@ -216,49 +216,9 @@ const { x, y, zoom } = board.viewport; board.setView({ x: x - event.deltaX, y: y - event.deltaY, zoom }); }; - // Two fingers on a touch screen, over the canvas or a window: the spot between them - // stays under them while their spread zooms the view and their travel pans it. One - // finger is left alone (it scrolls a window, or pans empty canvas through Svelte Flow). - // Until every finger lifts, moves go no further, so the finger left behind when the - // other lifts does not start a drag. Lifts still do: Svelte Flow saw the first finger - // land, and must see it go. - let pinch: { distance: number; mid: { x: number; y: number }; view: { x: number; y: number; zoom: number } } | null = null; - const spread = (touches: TouchList) => { - const [a, b] = [touches[0], touches[1]]; - const rect = element!.getBoundingClientRect(); - return { - distance: Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY) || 1, - mid: { x: (a.clientX + b.clientX) / 2 - rect.left, y: (a.clientY + b.clientY) / 2 - rect.top }, - }; - }; - const touch = (event: TouchEvent) => { - if (event.type === 'touchstart' && event.touches.length === 2) pinch = { ...spread(event.touches), view: { ...board.viewport } }; - if (!pinch) return; - if (event.type === 'touchstart' || event.type === 'touchmove') { - if (event.cancelable) event.preventDefault(); - event.stopPropagation(); - } - if (event.type === 'touchmove' && event.touches.length >= 2) { - const now = spread(event.touches); - const { view } = pinch; - const zoom = Math.min(ZOOM.max, Math.max(ZOOM.min, (view.zoom * now.distance) / pinch.distance)); - const at = { x: (pinch.mid.x - view.x) / view.zoom, y: (pinch.mid.y - view.y) / view.zoom }; - board.setView({ x: now.mid.x - at.x * zoom, y: now.mid.y - at.y * zoom, zoom }); - } - if (event.touches.length === 0) pinch = null; - }; - // Safari's own pinch would zoom the whole page instead. - const noPageZoom = (event: Event) => event.preventDefault(); - const touchTypes = ['touchstart', 'touchmove', 'touchend', 'touchcancel'] as const; - for (const type of touchTypes) element.addEventListener(type, touch, { passive: false, capture: true }); - element.addEventListener('gesturestart', noPageZoom); - element.addEventListener('gesturechange', noPageZoom); element.addEventListener('wheel', wheel, { passive: false, capture: true }); element.addEventListener('pointerdown', grab, { capture: true }); return () => { - for (const type of touchTypes) element?.removeEventListener(type, touch, { capture: true }); - element?.removeEventListener('gesturestart', noPageZoom); - element?.removeEventListener('gesturechange', noPageZoom); element?.removeEventListener('wheel', wheel, { capture: true }); element?.removeEventListener('pointerdown', grab, { capture: true }); }; @@ -325,11 +285,9 @@