Ask Petal answers in both languages, with room to read
The tutor prompt said "never mix languages in a single response" and mirrored the language of the question, so asking in English — which she does, because she is practising — returned the one explanation surface that gives nothing in her own language. It now answers in both, pair language first, halves separated by a blank line. Which half is the safety net and which is the lesson depends on who is writing: the pair is (English + X) and Petal is used from both ends, so the prompt asks for both and says it doesn't know which way round. The split is a rendering nicety, never a parse the reply depends on: a half-streamed reply is all one half, a model that ignores the instruction renders as one block, and nothing is ever dropped. For the height, the first attempt clamped the box to the room left below the anchored card so it could never overhang — measured, that gave 176px against a 442px answer, worse than the 220px it replaced. The card's own chrome spends ~290px of an 810px window, so "fits below the word" and "room to read" are not both available. The ceiling is now a flat 50vh and the overhang is made navigable instead, per item 4: the card reports its reach like the rail already does, the column grows, and the page can scroll to the actions below it.
This commit is contained in:
+132
-1
@@ -513,6 +513,119 @@ sane max-height (~50vh) before scrolling.
|
||||
**Acceptance:** an Ask Petal answer shows 中文 + English; a 3-paragraph
|
||||
answer is readable without scrolling a ~100 px box.
|
||||
|
||||
### 6 — DONE (eighth session). The answer was English-only on purpose, and the box was the smaller half of the item.
|
||||
|
||||
**The first half was one sentence in a prompt.** `askPetalSystemTemplate` said
|
||||
*"Detect the language of the user's message and respond in that same language…
|
||||
Never mix languages in a single response."* Self-consistent, and it made the
|
||||
English-only answer inevitable: ask in English — which she does, because she is
|
||||
practising — and the explanation that goes deepest into the "why" is the one
|
||||
surface that gives her nothing in her own language. It now asks for both halves
|
||||
every time, pair language first, and the old sentence is gone (a model handed
|
||||
both instructions picks one at random).
|
||||
|
||||
**Which half is the lesson is not Petal's to assume.** The first draft of this
|
||||
justified the change as "her language is the safety net, English is what she's
|
||||
learning" — wrong, and wrong in a way the code would have carried for good. The
|
||||
pair is (English + X) and Petal is used from both ends: an English speaker
|
||||
learning French needs the French half for exactly the reason a Mandarin speaker
|
||||
learning English needs the English one. So the prompt asks for both and says it
|
||||
does not know which way round, and nothing in the wording, the rendering or the
|
||||
comments assigns the halves a role. The ordering still holds either way — the
|
||||
pair language leads, English follows, which is the pack's order everywhere else.
|
||||
|
||||
**The blank line between the halves is a contract, and a soft one.**
|
||||
`bilingualReply.ts` splits on the first blank line to render the two halves the
|
||||
way the companion renders its two lines. It is deliberately forgiving because
|
||||
the reply streams in token by token from a small local model: a half-arrived
|
||||
reply is all "native" and the English simply appears beneath it when the break
|
||||
lands; a model that ignores the instruction renders as one ordinary block. The
|
||||
one thing it will never do is drop text. A separator with nothing on one side of
|
||||
it is a stray newline, not a split, and is kept whole.
|
||||
|
||||
- Petal's bubbles now take the card's full width. Two languages in the 85% a
|
||||
chat reserves to show who is talking wrapped a sentence into a paragraph, and
|
||||
the tint and alignment already say who is talking.
|
||||
- `chatFailed` moved into the packs. It is the only message the panel writes
|
||||
without the model, and it was English-only — telling the half of the pair that
|
||||
can't read English nothing at all, in the one situation where nothing else is
|
||||
on screen. It is written blank-line separated, so it renders through the same
|
||||
two-half bubble as a real reply.
|
||||
|
||||
**The height half was the larger one, and the first fix was wrong.** The item
|
||||
asks for ~50vh. Because the anchored card opens under the flagged word and never
|
||||
flips above it, the first version took the *smaller* of 50vh and the room left
|
||||
below the card, so it could never overhang the screen. Measured on the running
|
||||
build, that gave **176 px against a 442 px answer** — worse than the 220 px it
|
||||
replaced. The card's own pill, diff, explanation and action row already spend
|
||||
~290 px of an 810 px window: "fits below the word" and "room to read" are not
|
||||
both available, and the clamp silently chose the wrong one.
|
||||
|
||||
So the ceiling is flat 50vh and the overhang is made navigable instead — item 4's
|
||||
answer to the same conflict, in its own words: *make the overhang navigable, not
|
||||
shrink what each card says*. Both surfaces that host the panel now report their
|
||||
reach, so the column grows and the page can scroll to what hangs below:
|
||||
|
||||
- `SuggestionCard.tsx` — `onExtent`, a ResizeObserver rather than a one-shot
|
||||
measure, because the card grows twice after it mounts: the panel opens, and
|
||||
then the reply streams into it. It reports 0 as it unmounts.
|
||||
- `EditorCore.tsx` — `cardExtent` beside `railExtent`, resolved to one
|
||||
`overhang` (whichever reaches lower) that feeds the wrapper's `minHeight`. The
|
||||
rail's contribution stays conditional on the rail being mounted; the card's
|
||||
does not, because it withdraws its own.
|
||||
- The rail needed nothing: it already re-measures on `expandedId` and a
|
||||
per-card ResizeObserver, so a rail card whose conversation grows reports it.
|
||||
|
||||
**Verified in a real browser at the review's own 1517×810**, against a stand-in
|
||||
model server (no VPN, no GPU) returning a deliberately long three-paragraph
|
||||
bilingual reply:
|
||||
|
||||
- *Anchored card.* Box **176 → 362 px** (50vh), card overhangs by 165 px, the
|
||||
column gained 209 px of scroll where the same card previously had 20, and
|
||||
Accept is fully on screen after scrolling to it. No horizontal overflow.
|
||||
- *Rail card.* Box 334 px, overhang 104 px, 204 px of scroll room, Accept
|
||||
reachable. The rail's own extent pipeline covered it, as read.
|
||||
- *On a taller window* the whole 494 px answer fits with no scrollbar at all.
|
||||
|
||||
**Honest limit:** at 810 px a genuinely long answer still scrolls — 362 px of
|
||||
442. The item's "readable without scrolling a ~100 px box" is met in the sense
|
||||
that mattered (the box is no longer a peephole and the rest is a short scroll,
|
||||
not a hunt), but a three-paragraph *bilingual* answer is roughly twice the text
|
||||
the item imagined, and half a small screen does not hold it.
|
||||
|
||||
**A trap worth recording, because it cost two false measurements.** The restart
|
||||
script used `pkill -f 'scratchpad/petal$'`, which never matched: the process was
|
||||
started as `./petal` after a `cd`, so its command line doesn't contain the path.
|
||||
Every "restart" after the first therefore failed to bind the port and died
|
||||
quietly, while the *original* binary kept serving the *original* bundle — and
|
||||
the page loaded fine, the app worked, and the numbers looked plausible. Two
|
||||
rounds of "the fix didn't take" were measurements of code that was never
|
||||
running. What caught it was the inline `max-height` reading `176.417px`, a value
|
||||
the new code cannot produce. **Check the served bundle hash, not that the page
|
||||
loads** (`curl -s localhost:PORT/ | grep -o 'index-[A-Za-z0-9_-]*\.js'` against
|
||||
`web/dist/index.html`).
|
||||
|
||||
**Deliberately not done:**
|
||||
|
||||
- No auto-scroll to the card when the panel opens. The conversation starts short
|
||||
and grows; scrolling the page out from under her the moment she clicks Ask
|
||||
Petal would move the sentence she is reading about, to solve a problem she
|
||||
does not have yet.
|
||||
- The seed bubble stays single-language. It is the pair-language rendering of
|
||||
the English explanation printed directly above it in the same card — the card
|
||||
is already bilingual across those two lines, and repeating the English inside
|
||||
the bubble would be the "same text twice" the seed exists to avoid.
|
||||
|
||||
Coverage: `bilingualReply.test.ts` (both scripts, mid-stream, one-language,
|
||||
extra blank lines, whitespace-only separator, empty, single newlines inside a
|
||||
half), `lang_test.go`'s `TestAskPetalAnswersInBothLanguages` (all four pairs ask
|
||||
for both languages, name the pair language first, keep the separator, and no
|
||||
longer carry the sentence forbidding it), and an `i18n.test.ts` case that every
|
||||
pack's `chatFailed` has two non-empty halves and keeps its English one in
|
||||
English. **The extent wiring has no unit test**, for the reason item 7 recorded:
|
||||
jsdom has no layout, every rect is zero, and a test there would pass whatever
|
||||
the code did. It is browser-verified only, and is written down as such.
|
||||
|
||||
## 7. Inline popover at the underline (verify, then strengthen)
|
||||
|
||||
Grammarly's core gesture is click-the-word → popup at the word.
|
||||
@@ -714,6 +827,16 @@ recorded: **a fixed-width column that gets re-centred changes position without
|
||||
changing size**, and neither a ResizeObserver on it nor a window resize will say
|
||||
so. Untouched: 6, 8, item 3's incremental half.)*
|
||||
|
||||
*(Eighth session: item 6 done — see the subsection under it. Two things to carry
|
||||
forward. First, **the pair is symmetric**: Petal is used from both ends, so
|
||||
"her language" and "the language being learned" are not interchangeable terms,
|
||||
and any copy or prompt that assigns the two halves a role is wrong for half the
|
||||
users — the wording here was corrected mid-session for exactly that. Second,
|
||||
**check the served bundle hash before believing a browser measurement**: a
|
||||
restart that silently failed left an old binary serving an old bundle through
|
||||
two rounds of measurement, and nothing about the running app looked wrong.
|
||||
Untouched: 8, item 3's incremental half.)*
|
||||
|
||||
**Migration 0015 on the live database.** It rebuilds the suggestions table, so
|
||||
unlike 0014 it could have dropped her rows. Backed up first — and the backup
|
||||
had to be the whole WAL set (`petal.db`, `-wal`, `-shm` in
|
||||
@@ -728,7 +851,15 @@ CHECK, and every existing row still carrying the label she has already read (the
|
||||
seven `clarity` rows include the mislabelled Chinese one — by design, only new
|
||||
findings get the new type; if you want that card relabelled, edit the sentence).
|
||||
|
||||
**Suggested next (seventh session onward):** **item 6** is the obvious pick —
|
||||
**Suggested next (eighth session onward):** only **item 8**'s four small ones
|
||||
and item 3's incremental half remain. **Dismissal persistence** is still the one
|
||||
with real value now that item 2 gives suggestions stable identity across checks;
|
||||
the status-bar summary is the cheapest. Item 3's incremental surfacing needs
|
||||
streaming, which the current `/check` response shape doesn't do — it remains the
|
||||
largest of what's left.
|
||||
|
||||
*(Superseded, kept for the reading list: the seventh session recommended item 6,
|
||||
which is now done.)* **item 6** was the obvious pick —
|
||||
it is the last small one, it is self-contained (prompt the answer path to reply
|
||||
in both pair languages, then let the answer area grow to ~50vh instead of a
|
||||
~100px scrollbox), and item 7 just made the surface it lives on more prominent:
|
||||
|
||||
Reference in New Issue
Block a user