diff --git a/CLAUDE.md b/CLAUDE.md index c5e7b3d..8db2d05 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,9 +47,9 @@ A dark-only (no light mode, no theme toggle) Electron desktop code workbench for - **Prism PHP load order:** `prism-php` requires `prism-markup-templating` to be loaded **first**, or every `Prism.highlight` call throws and silently falls back to plain text. - **Preload must be CommonJS `index.cjs`** and `main` must load `../preload/index.cjs` (see `electron.vite.config.ts` preload `rollupOptions.output`). If they mismatch (or you let it build as `.mjs`), Electron silently loads no preload, `window.helder` is undefined, and the renderer silently falls back to mock data + the "terminal not available" message. Keep the filename and the `main` path in sync. - **chokidar is pinned to v3 on purpose — do NOT bump to v4/v5.** chokidar ≥4 dropped the `fsevents` addon and watches recursively via libuv's native `fs.watch({recursive:true})`. On macOS that recursive watcher poisons the process's file descriptors, so every later `child_process.spawn` (i.e. every `git` call) fails with `spawn EBADF` (errno -9) and the git column silently stops updating. v3 uses the `fsevents` native addon instead and has no such conflict. If you must move to v4+, switch the main project watcher to `usePolling: true` (the only other config proven to avoid the EBADF here). -- **Word wrap swaps the buffer's layout, it is not just a CSS switch** (`editor.wordWrap`: `markdown` default / `on` / `off`). Unwrapped, `CodeEditor` renders one highlighted blob and a separate gutter column that follows the scroll. Wrapped, a fixed 20px-per-line gutter no longer lines up, so each line becomes a `.ce-line` block and the number is a CSS counter on `::before` — that keeps it on the first visual row and leaves folded rows blank. Two things bite here: `.ce-inner` must drop `width:max-content` (and `.editor` its `max-content` grid track), or a folded line still measures its full unfolded width and never breaks; and the textarea and the `
` must fold identically — same width, padding, font, `white-space:pre-wrap`, `overflow-wrap:break-word` — or the caret drifts off the text. The full-screen Diff never wraps on purpose: its two panes align row by row. The hover original panel must fold at the same points as the editor, or the old line does not stay level with the current line.
+- **Word wrap swaps the buffer's layout, it is not just a CSS switch** (`editor.wordWrap`: `markdown` default / `on` / `off`). Unwrapped, `CodeEditor` renders one highlighted blob and a separate gutter column that follows the scroll. Wrapped, a fixed 20px-per-line gutter no longer lines up, so each line becomes a `.ce-line` block and the number is a CSS counter on `::before` — that keeps it on the first visual row and leaves folded rows blank. Two things bite here: `.ce-inner` must drop `width:max-content` (and `.editor` its `max-content` grid track), or a folded line still measures its full unfolded width and never breaks; and the textarea and the `` must fold identically — same width, padding, font, `white-space:pre-wrap`, `overflow-wrap:break-word` — or the caret drifts off the text. The full-screen Diff never wraps on purpose: its two panes align row by row. The original panel must fold at the same points as the editor, or the old line does not stay level with the current line.
- **Pass on to Agent uses bracketed paste.** Write inserts to the agent PTY wrapped in `\x1b[200~ … \x1b[201~` so the `claude` CLI treats it as *pasted, unsubmitted* input. Insert must never submit — it lands as a new line so the user can stack several references before sending.
-- **The three view modes (Actual / Original / Diff), plus Preview for markdown, all derive from one original-text + updated-text pair per changed file.** The prototype computes the pair with an LCS line diff (`buildDiff()` in `design/src/data.js`); production should prefer real `git diff` output. **Actual is the writable buffer, and it marks the changed lines in place.** A marked row takes the teal `--add` tint, a 2px `--add` left rule, and a teal line number. There is no `+` glyph, no sign column, and no second row. The removed lines appear on hover: `mouseenter` on a marked line opens a 496px panel with a 2px `--del` left border over the agent + terminal column, and `mouseleave` closes it. The panel holds the whole original file and scrolls so the previous version of the hovered line sits level with the hovered line. There is no click, no pin, and no animation. A pure deletion has no current line to mark, so the neighbouring current line takes a 2px `--del` rule on its edge and opens the same panel. **Diff is the full-screen side-by-side view**: it covers the whole application, original left, updated right, lines aligned, and `Esc` returns to the previous mode. The old separate **Split** button is gone, and the `Diff` segment opens that view instead. `git.defaultDiffMode` defaults to `'updated'` (Actual), so a click on a changed git row opens the file in Actual. The git context menu's **Open diff** is the explicit way into the full-screen view. The colour language is token-based everywhere: **teal `--add` is what the file holds now, amber-deep `--del` is what it held before** — never green, never red. Syntax highlighting stays on in all modes. The design source for the marked lines and the hover panel is `design_handoff_helder_inline_diff/` (README.md + `03b-in-pane-diff.html`).
+- **The three view modes (Actual / Original / Diff), plus Preview for markdown, all derive from one original-text + updated-text pair per changed file.** The prototype computes the pair with an LCS line diff (`buildDiff()` in `design/src/data.js`); production should prefer real `git diff` output. **Actual is the writable buffer, and it marks the changed lines in place.** A marked row takes the teal `--add` tint, a 2px `--add` left rule, and a teal line number. There is no `+` glyph, no sign column, and no second row. The removed lines appear on a click: a click on a marked line opens a 496px panel with a 2px `--del` left border over the agent + terminal column. The panel holds the whole original file and scrolls so the previous version of the picked line sits level with the picked line. A second click on the same line, or a click anywhere off the code, closes it. Hover does nothing, and there is no animation. A pure deletion has no current line to mark, so the neighbouring current line takes a 2px `--del` rule on its edge and opens the same panel. **Diff is the full-screen side-by-side view**: it covers the whole application, original left, updated right, lines aligned, and `Esc` returns to the previous mode. The old separate **Split** button is gone, and the `Diff` segment opens that view instead. `git.defaultDiffMode` defaults to `'updated'` (Actual), so a click on a changed git row opens the file in Actual. The git context menu's **Open diff** is the explicit way into the full-screen view. The colour language is token-based everywhere: **teal `--add` is what the file holds now, amber-deep `--del` is what it held before** — never green, never red. Syntax highlighting stays on in all modes. The design source for the marked lines and the original panel is `design_handoff_helder_inline_diff/` (README.md + `03b-in-pane-diff.html`).
- **The agent pane is just a terminal running the `claude` CLI** (`ai.command`, default `claude`, auto-launched when `ai.autoLaunch` is on). The bottom pane is a normal shell PTY. The prototype's simulated agent session (`agentSeed`, `runAgent`, `bootAgent` in `terminals.jsx`) exists only to show the visual style — keep the styling, drop the fakery.
- **Chrome budget:** title bar + tab strip + panel headers + status bar combined should stay ≈10% of vertical height. Keep it minimal.
- **`console.*` is not a log — use the logger.** Helder runs one process per project window, and every window past the first is spawned by `spawnInstance()` with `stdio: 'ignore'`; launched from Finder there's no terminal either. Console output is therefore discarded in real use. Log through `src/main/logger.ts` (main) or `src/renderer/src/log.ts` → `rlog` (renderer, forwarded over IPC to the same file). Never add a bare `catch {}` on an IPC/FS/git path: log the cause, then handle it.
@@ -104,7 +104,7 @@ The older `design_handoff_helder_workbench/` prototype is superseded by `docs/de
1. Electron shell + frameless dark window; port tokens to CSS vars; bundle IBM Plex.
2. Static layout: four resizable columns + title/status bars.
3. Real file tree + open files into tabs (read-only) with Prism highlighting.
-4. Git panel from `git status` (read-only) → staging + commit → the three view modes, the hover original panel, and the full-screen Diff.
+4. Git panel from `git status` (read-only) → staging + commit → the three view modes, the original panel, and the full-screen Diff.
5. Search (ripgrep + fuzzy).
6. Terminals via node-pty + xterm.js; run `claude` in the agent pane.
7. Copy reference + Pass-on-to-Agent (clipboard + bracketed-paste into the agent PTY).
diff --git a/DESIGN.md b/DESIGN.md
index f72cc04..dccfcbd 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -136,7 +136,7 @@ All are presentations of the same change set for the file:
3. **Diff**: the editor expands to full screen, covering the other columns. The original file is on the left and the updated file is on the right, lines aligned. `Esc`, or a collapse control in the corner, returns to the normal layout and the previously active mode.
4. **Preview**: markdown files only. It shows the rendered document instead of the source.
-**In Actual, the removed lines appear on hover.** The pointer enters a marked line, and a panel opens over the agent and terminal column: 496px wide, with a 2px amber-deep left border. The panel holds the whole original file, and it scrolls so that the previous version of the hovered line sits level with the hovered line. The pointer leaves the line, and the panel closes. There is no click, no pin, and no animation.
+**In Actual, the removed lines appear on a click.** The user clicks a marked line, and a panel opens over the agent and terminal column: 496px wide, with a 2px amber-deep left border. The panel holds the whole original file, and it scrolls so that the previous version of the picked line sits level with the picked line. A second click on the same line closes the panel, and so does a click anywhere off the code. Hover does nothing, and there is no animation.
Shared rules: teal is what the file holds now, amber-deep is what it held before. There is no green and no red anywhere. Syntax highlighting stays on in all modes. Line numbers follow `editor.lineNumbers` (default absolute).
@@ -144,7 +144,7 @@ Shared rules: teal is what the file holds now, amber-deep is what it held before
- A file with no changes opens directly in a normal editable view with no view-mode control, since there is nothing to diff.
- Editing follows the editor settings: indentation, indent width, trim trailing whitespace, insert final newline, word wrap, indent guides, whitespace rendering, and active-line highlight, all read from configuration (section 10).
-- **Word wrap** follows `editor.wordWrap`: `markdown` (default), `on`, or `off`. When it is on, a line too wide for the pane folds onto the next row instead of scrolling sideways. Only the first row of a folded line carries a line number; the rows below it stay blank. The hover panel folds at the same points as the editor, so each old line stays level with its current line. Diff never wraps, because its two panes align row by row.
+- **Word wrap** follows `editor.wordWrap`: `markdown` (default), `on`, or `off`. When it is on, a line too wide for the pane folds onto the next row instead of scrolling sideways. Only the first row of a folded line carries a line number; the rows below it stay blank. The original panel folds at the same points as the editor, so each old line stays level with its current line. Diff never wraps, because its two panes align row by row.
### Right-click in code
diff --git a/src/renderer/src/editor.tsx b/src/renderer/src/editor.tsx
index ec60633..5a6e1c5 100644
--- a/src/renderer/src/editor.tsx
+++ b/src/renderer/src/editor.tsx
@@ -82,7 +82,7 @@ const PAD_TOP = 10
* the number is a CSS counter on `::before`, which keeps it on the first
* visual row and leaves the folded rows unnumbered. Highlighting is then per
* line (like the diff views), so multi-line tokens don't carry over. */
-function CodeEditor({ path, text, lang, wrap, flash, marks, onHover, onChange, onContext, onSymbol }: {
+function CodeEditor({ path, text, lang, wrap, flash, marks, onPick, onChange, onContext, onSymbol }: {
path: string
text: string
lang: string | null
@@ -92,7 +92,7 @@ function CodeEditor({ path, text, lang, wrap, flash, marks, onHover, onChange, o
/** Changed lines of this file, from the HEAD-vs-disk diff. */
marks: ChangeMark[]
/** The marked line under the pointer, with its top in the pane's viewport. */
- onHover: (h: { line: number; top: number } | null) => void
+ onPick: (h: { line: number; top: number } | null) => void
onChange: (text: string) => void
onContext: OnContext
onSymbol: OnSymbol
@@ -158,31 +158,32 @@ function CodeEditor({ path, text, lang, wrap, flash, marks, onHover, onChange, o
}))
}, [marks, wrap, text, paneW])
- /* The hover reveal. The pointer lands on the textarea rather than on a row, so
- * the line comes from the bands — the same geometry that drew the tint. */
- const hoverRef = useRef(null)
+ /* The reveal of the old lines. A click pins it, a second click on the same
+ * band or a click off every band drops it. The click lands on the textarea
+ * rather than on a row, so the line comes from the bands — the same geometry
+ * that drew the tint. */
+ const pickRef = useRef(null)
function report(list: Band[], line: number | null): void {
const b = line == null ? undefined : list.find((x) => x.line === line)
- hoverRef.current = b ? b.line : null
- onHover(b && scrollRef.current ? { line: b.line, top: b.top - scrollRef.current.scrollTop } : null)
+ pickRef.current = b ? b.line : null
+ onPick(b && scrollRef.current ? { line: b.line, top: b.top - scrollRef.current.scrollTop } : null)
}
- function onMove(e: React.MouseEvent): void {
- if (!bands.length && hoverRef.current == null) return
+ function pickAt(e: React.MouseEvent): void {
+ if (!bands.length && pickRef.current == null) return
const box = innerRef.current?.getBoundingClientRect()
if (!box) return
const y = e.clientY - box.top
const hit = bands.find((b) => y >= b.top && y < b.top + b.height)
- const line = hit ? hit.line : null
- if (line !== hoverRef.current) report(bands, line)
+ report(bands, hit && hit.line !== pickRef.current ? hit.line : null)
}
- // An edit moves the bands under a held pointer; the panel follows them.
+ // An edit moves the bands under the pinned line; the panel follows them.
// eslint-disable-next-line react-hooks/exhaustive-deps
- useLayoutEffect(() => { if (hoverRef.current != null) report(bands, hoverRef.current) }, [bands])
+ useLayoutEffect(() => { if (pickRef.current != null) report(bands, pickRef.current) }, [bands])
function onScroll(): void {
const s = scrollRef.current
if (s && gutterRef.current) gutterRef.current.style.transform = `translateY(${-s.scrollTop}px)`
- if (hoverRef.current != null) report(bands, hoverRef.current)
+ if (pickRef.current != null) report(bands, pickRef.current)
}
// The textarea is overflow-hidden under the scroller, so keep the caret line
// in view by scrolling the container ourselves (6px top pad, 20px line-height).
@@ -279,10 +280,9 @@ function CodeEditor({ path, text, lang, wrap, flash, marks, onHover, onChange, o
)}
- {/* No marks, no hover: an unchanged file pays nothing for the reveal. */}
- { if (hoverRef.current != null) report(bands, null) } : undefined}>
+ {/* No marks, no pick: an unchanged file pays nothing for the reveal. */}
+ { if (e.metaKey || e.ctrlKey) handleTokenClick(e); else if (marks.length) pickAt(e) }}>
{bands.map((b) => (
@@ -305,7 +305,7 @@ function CodeEditor({ path, text, lang, wrap, flash, marks, onHover, onChange, o
)
}
-/* The file has no HEAD version. Shown by Original mode and by the hover
+/* The file has no HEAD version. Shown by Original mode and by the picked
* overlay, so both say the same thing. */
function NoOriginal(): React.ReactElement {
return (
@@ -474,7 +474,7 @@ function PaneView({ cacheKey, path, lines, lang, showSign, wrap, cursor, selecti
}
/** The original lines behind one current line: the range to mark amber, and the
- * one to line up with the hovered row. */
+ * one to line up with the picked row. */
interface PeekTarget {
anchor: number
from: number | null
@@ -484,7 +484,7 @@ interface PeekTarget {
/* Actual: the current file with the changed lines marked in place. Removals own
* no line here, so a run of them puts its rule on the edge of the line that took
* its place — unless that line is itself an added one, which already says the
- * same thing in teal. The map is what the hover overlay reads. */
+ * same thing in teal. The map is what the overlay reads. */
export function buildDiffView(diff: Diff): { lines: ViewLine[]; peek: Map } {
const peek = new Map()
const above = new Set()
@@ -628,14 +628,25 @@ export function Editor({ active, mode, side, setMode, onContext, onPeek, cursor,
// Editable in the live-buffer modes; Original/Preview stay read-only views.
const editable = paneMode === 'code' || paneMode === 'updated'
- /* The hover reveal of Actual. CodeEditor owns the geometry, because over a
- * textarea there is no row to read the pointer off; `top` is the hovered
+ /* The pinned reveal of Actual. CodeEditor owns the geometry, because over a
+ * textarea there is no row to read the pointer off; `top` is the picked
* line's offset inside the pane viewport, and the overlay aligns on it. */
- const [hover, setHover] = useState<{ line: number; top: number } | null>(null)
- const onHoverLine = useCallback((h: { line: number; top: number } | null) => setHover(h), [])
- useEffect(() => { setHover(null) }, [active, effMode, side])
+ const [pick, setPick] = useState<{ line: number; top: number } | null>(null)
+ const onPickLine = useCallback((h: { line: number; top: number } | null) => setPick(h), [])
+ useEffect(() => { setPick(null) }, [active, effMode, side])
+ /* A click anywhere but the code drops the panel. The editor's own clicks are
+ * already handled there, and the panel takes no pointer, so a click on it
+ * lands on the column below and counts as "somewhere else". */
+ useEffect(() => {
+ if (!pick) return
+ function away(e: MouseEvent): void {
+ if (!(e.target as HTMLElement | null)?.closest?.('.ce-scroll')) setPick(null)
+ }
+ document.addEventListener('mousedown', away, true)
+ return () => document.removeEventListener('mousedown', away, true)
+ }, [pick])
- const target = hover && diffView ? diffView.peek.get(hover.line) ?? null : null
+ const target = pick && diffView ? diffView.peek.get(pick.line) ?? null : null
const peekLines = useMemo(() => (target && diff
? diff.left.map((l) => ({
no: l.no,
@@ -644,10 +655,10 @@ export function Editor({ active, mode, side, setMode, onContext, onPeek, cursor,
}))
: []), [diff, target])
useEffect(() => {
- onPeek(active && target && hover
- ? { path: active, lines: peekLines, lang, wrap, anchor: target.anchor, top: hover.top, empty: peekLines.length === 0 }
+ onPeek(active && target && pick
+ ? { path: active, lines: peekLines, lang, wrap, anchor: target.anchor, top: pick.top, empty: peekLines.length === 0 }
: null)
- }, [active, target, hover, peekLines, lang, wrap, onPeek])
+ }, [active, target, pick, peekLines, lang, wrap, onPeek])
useEffect(() => () => onPeek(null), [onPeek])
return (
@@ -702,7 +713,7 @@ export function Editor({ active, mode, side, setMode, onContext, onPeek, cursor,
) : editable ? (
+ onPick={onPickLine} onChange={onEdit} onContext={onContext} onSymbol={onSymbol} />
) : (
built && undefined
/* The original file, over the agent column and level with the editor. It takes
- * no pointer of its own, so the panel can never steal the hover from the row
- * that opened it. */
+ * no pointer of its own, so a click on it counts as a click outside the code and
+ * closes the panel. */
export function OriginalPeek({ path, lines, lang, wrap, anchor, top, empty }: PeekInfo): React.ReactElement {
const bodyRef = useRef(null)
// Measured, not counted: a folded line is taller than one row, so only the
@@ -749,7 +760,7 @@ export function OriginalPeek({ path, lines, lang, wrap, anchor, top, empty }: Pe
Original
before this change · same scroll
- hold hover
+ click the line again to close
{empty ? : (
diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css
index 22e87ac..85fdda4 100644
--- a/src/renderer/src/styles.css
+++ b/src/renderer/src/styles.css
@@ -492,8 +492,8 @@ kbd { flex:0 0 auto; font-family:var(--mono); font-size:12px; font-weight:600; c
.ce-line.chg::before { color:var(--add); }
/* The hover reveal: the whole original file over the agent column, level with
- the editor. It takes no pointer, so it can never break the hover that opened
- it. 496px is the design width of that column plus its 2px rule. */
+ the editor. It takes no pointer, so a click on it counts as a click outside
+ the code and closes the panel. 496px is the design width of that column plus its 2px rule. */
.peek {
position:absolute; top:0; right:0; bottom:0; width:496px; z-index:40;
display:flex; flex-direction:column; overflow:hidden; pointer-events:none;
@@ -826,9 +826,12 @@ kbd { flex:0 0 auto; font-family:var(--mono); font-size:12px; font-weight:600; c
/* xterm.js host (real terminals) */
/* The pane carries the colour, taken from the terminal palette in config.json,
so the padding around the canvas can never sit on a different ground. */
-.term-xterm { flex:1; min-height:0; overflow:hidden; padding:10px 6px 10px 12px; background:var(--bg-0); }
+.term-xterm { flex:1; min-height:0; overflow:hidden; padding:12px; background:var(--bg-0); }
.term-xterm .xterm { height:100%; }
-.term-xterm .xterm-viewport { background:transparent !important; }
+/* The scrollbar takes layout width inside the viewport, so at right:0 it would
+ stand on the last column of text. Pushed into the right padding it overlays
+ empty ground, and the grid keeps all four gaps equal (see fitTerm). */
+.term-xterm .xterm-viewport { background:transparent !important; right:-9px; }
.term-xterm .xterm-viewport::-webkit-scrollbar { width:9px; }
.term-xterm .xterm-viewport::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.13); border-radius:5px; border:2px solid transparent; background-clip:content-box; }
.term-xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { background:rgba(255,255,255,0.22); background-clip:content-box; }
diff --git a/src/renderer/src/terminals.tsx b/src/renderer/src/terminals.tsx
index a207e00..8960c53 100644
--- a/src/renderer/src/terminals.tsx
+++ b/src/renderer/src/terminals.tsx
@@ -6,7 +6,6 @@
* references can be stacked before the user hits Enter. */
import React, { useEffect, useRef, useState } from 'react'
import { Terminal as XTerm } from '@xterm/xterm'
-import { FitAddon } from '@xterm/addon-fit'
import '@xterm/xterm/css/xterm.css'
import { ContextMenu } from './overlays'
import type { Menu } from './overlays'
@@ -45,10 +44,41 @@ function xtermOptions(cfg: HelderConfig['terminal']): {
}
}
+/* Gap between the grid and the pane edge, in px. */
+const PAD = 12
+type Cell = { width: number; height: number }
+
+function cellSize(term: XTerm): Cell | null {
+ const core = (term as unknown as {
+ _core?: { _renderService?: { dimensions?: { css?: { cell?: Cell } } } }
+ })._core
+ const cell = core?._renderService?.dimensions?.css?.cell
+ return cell && cell.width > 0 && cell.height > 0 ? cell : null
+}
+
+/* Sizes the grid to the host and centres it. FitAddon is not used here: it
+ * reserves 14px on the right for an overview ruler we never draw, and it leaves
+ * the sub-cell remainder on one side, so the right and bottom gaps came out far
+ * wider than the left and top. Both measurements come from clientWidth, which
+ * counts the padding, so writing the padding back cannot change the result and
+ * the ResizeObserver settles after one pass. */
+function fitTerm(term: XTerm, host: HTMLElement): void {
+ const cell = cellSize(term)
+ if (!cell) return
+ const innerW = host.clientWidth - PAD * 2
+ const innerH = host.clientHeight - PAD * 2
+ const cols = Math.max(2, Math.floor(innerW / cell.width))
+ const rows = Math.max(1, Math.floor(innerH / cell.height))
+ if (cols !== term.cols || rows !== term.rows) term.resize(cols, rows)
+ const x = PAD + Math.max(0, innerW - cols * cell.width) / 2
+ const y = PAD + Math.max(0, innerH - rows * cell.height) / 2
+ host.style.padding = `${y}px ${x}px`
+}
+
export function Terminal({ kind }: { kind: 'agent' | 'shell' }): React.ReactElement {
const hostRef = useRef(null)
const termRef = useRef(null)
- const fitRef = useRef(null)
+ const fitRef = useRef<(() => void) | null>(null)
// The terminal is built once and lives as long as its PTY, so the config is
// read through a ref on creation and re-applied by the effect below — editing
// config.json restyles the running session instead of restarting it.
@@ -69,10 +99,9 @@ export function Terminal({ kind }: { kind: 'agent' | 'shell' }): React.ReactElem
const term = new XTerm({ ...xtermOptions(cfgRef.current), allowProposedApi: true })
termRef.current = term
- const fit = new FitAddon()
- fitRef.current = fit
- term.loadAddon(fit)
term.open(host)
+ const refit = (): void => fitTerm(term, host)
+ fitRef.current = refit
// Both panes (D1 agent + D2 shell): selecting text auto-copies it to the clipboard.
if (bridge) {
@@ -90,7 +119,7 @@ export function Terminal({ kind }: { kind: 'agent' | 'shell' }): React.ReactElem
term.selectAll()
return false
})
- try { fit.fit() } catch { /* host not measured yet */ }
+ try { refit() } catch { /* host not measured yet */ }
let disposed = false
let id = -1
@@ -150,7 +179,7 @@ export function Terminal({ kind }: { kind: 'agent' | 'shell' }): React.ReactElem
if (frame) return
frame = requestAnimationFrame(() => {
frame = 0
- try { fit.fit() } catch { /* host not measured yet */ }
+ try { refit() } catch { /* host not measured yet */ }
})
})
ro.observe(host)
@@ -176,7 +205,7 @@ export function Terminal({ kind }: { kind: 'agent' | 'shell' }): React.ReactElem
const term = termRef.current
if (!term) return
Object.assign(term.options, xtermOptions(cfg))
- fitRef.current?.fit()
+ fitRef.current?.()
}, [cfg])
// Both panes (D1 agent + D2 shell): right-click → copy selection / paste from clipboard.
diff --git a/test/deleted-lines.test.tsx b/test/deleted-lines.test.tsx
index b9f66f1..d1330ef 100644
--- a/test/deleted-lines.test.tsx
+++ b/test/deleted-lines.test.tsx
@@ -52,7 +52,7 @@ describe('a change that only removes a line', () => {
expect(c.querySelector('.ce-gutter .chg')).toBeNull()
})
- it('hands the removed line to the hover panel', async () => {
+ it('hands the removed line to the picked panel', async () => {
const c = await openRow()
const scroll = await waitFor(() => {
const el = c.querySelector('.ce-scroll')
@@ -60,7 +60,7 @@ describe('a change that only removes a line', () => {
return el
})
// 'tail' took the place of the removed line, and sits on line 2.
- fireEvent.mouseMove(scroll, { clientY: 10 + 20 + 5 })
+ fireEvent.click(scroll, { clientY: 10 + 20 + 5 })
const peek = await waitFor(() => {
const p = c.querySelector('.peek')
if (!p) throw new Error('peek not ready')
diff --git a/test/editor-modes.test.tsx b/test/editor-modes.test.tsx
index 5b2820c..12779e3 100644
--- a/test/editor-modes.test.tsx
+++ b/test/editor-modes.test.tsx
@@ -74,7 +74,7 @@ describe('Editor view modes', () => {
expect(find(c, '.seg button.on', 'Original')).toBeTruthy()
})
- it('hovering a band in Actual reveals the original, leaving the editor hides it', async () => {
+ it('clicking a band in Actual reveals the original, clicking it again hides it', async () => {
const c = await openChanged()
const scroll = await waitFor(() => {
const el = c.querySelector('.ce-scroll')
@@ -83,7 +83,7 @@ describe('Editor view modes', () => {
})
// Line 30 is the first rewritten line. Unwrapped, its band is arithmetic:
// a 10px top pad plus 20px per line, and the hit test reads clientY.
- fireEvent.mouseMove(scroll, { clientY: 10 + 29 * 20 + 5 })
+ fireEvent.click(scroll, { clientY: 10 + 29 * 20 + 5 })
const peek = await waitFor(() => {
const p = c.querySelector('.peek')
if (!p) throw new Error('peek not ready')
@@ -93,7 +93,8 @@ describe('Editor view modes', () => {
expect(removed).toHaveLength(1)
expect(removed[0].textContent).toContain("'plan' => $user->plan,")
- fireEvent.mouseLeave(scroll)
+ // A second click on the same band drops it.
+ fireEvent.click(scroll, { clientY: 10 + 29 * 20 + 5 })
await waitFor(() => expect(c.querySelector('.peek')).toBeNull())
})
diff --git a/test/git-two-rows.test.tsx b/test/git-two-rows.test.tsx
index f606f92..44812e2 100644
--- a/test/git-two-rows.test.tsx
+++ b/test/git-two-rows.test.tsx
@@ -103,7 +103,7 @@ describe('a file that is staged and then edited again', () => {
expect(splitText(c, 'right')).toEqual(['a', 'STAGED', 'c', 'AFTER-STAGING'])
})
- it('the hover panel behind Actual always reaches back to HEAD', async () => {
+ it('the picked panel behind Actual always reaches back to HEAD', async () => {
const c = await boot()
fireEvent.click(group(c, 'Changes')[0])
const scroll = await waitFor(() => {
@@ -112,7 +112,7 @@ describe('a file that is staged and then edited again', () => {
return el
})
// Line 2 is 'STAGED'. Unwrapped, its band runs from 10 + (2-1)*20.
- fireEvent.mouseMove(scroll, { clientY: 10 + 20 + 5 })
+ fireEvent.click(scroll, { clientY: 10 + 20 + 5 })
const peek = await waitFor(() => {
const p = c.querySelector('.peek')
if (!p) throw new Error('peek not ready')
@@ -121,7 +121,8 @@ describe('a file that is staged and then edited again', () => {
const removed = Array.from(peek.querySelectorAll('.ln-row.del'))
expect(removed.map((el) => el.textContent?.replace(/^\d+/, ''))).toEqual(['b'])
- fireEvent.mouseLeave(scroll)
+ // A click anywhere off the code drops it.
+ fireEvent.mouseDown(document.body)
await waitFor(() => expect(c.querySelector('.peek')).toBeNull())
})