8 Commits
Author SHA1 Message Date
sorokin.oleg.edu a50ca4b867 feat: add Russian localization and independent STT languages
CI / Typecheck, tests, lint (push) Canceled after 0s
CI / Standalone (mac-arm64) (push) Canceled after 0s
CI / Standalone (linux-x64) (push) Canceled after 0s
CI / Standalone (windows-x64) (push) Canceled after 0s
2026-10-25 17:18:44 +03:00
amosblomqvist bc1f668e2a Extraction surfaces: sidebar coverage block, chat-bar ring, phase un-pill, panel death
The owner-locked extraction-UX redesign (dispatch 2026-09-24), UI
chunk over Platform's data layer (eed4b4e — GET /api/chats/[id]
carries `coverage`, the threshold is Graph ▸ extractionChunkTokens):

1. SIDEBAR BLOCK (components/extraction-coverage.tsx +
   sidebar's session menu): the coverage info block at the TOP of
   the context menu — uncovered tokens, uncovered messages, running
   state — then the Extract affordance (optimistic "extracting… /
   Running" until the server says running; 409 = benign, the trigger
   raced us), fetched ON MENU OPEN (the block mounts with the menu —
   on-demand, never eagerly), polled at 2s while a run is in flight.
   Visually separated from Rename/Delete by a hairline; those stay
   the two session-management actions. Failed read → nothing (never
   fake zeros).

2. RING METER (CoverageRing, chat top bar, before the PhaseChip):
   a 16px donut filling uncoveredTokens / extractionChunkTokens —
   "how close this session is to its next extraction run". Click →
   the exact numbers (covered/uncovered tokens, messages, the
   threshold, lastFoldedAt via timeAgo). Refresh chains: mount, the
   turn-settle bump (chat.tsx coverageTick), the settings signal
   (threshold/kill-switch re-read live), a 2s drain-poll while a run
   is in flight, and the ACTIVITY FEED — a run for this session
   appearing or settling in the dock's feed re-reads coverage (runs
   fire from other surfaces — the sidebar menu, the turn pipeline —
   and no turn-settle bump announces them; the runs-empty-transition
   the graph view rides). Vanishes entirely when the graph is off or
   before the first honest read. Running = the whole ring PULSES
   (implementer's visual call: motion is the working-state signal in
   this app's quiet register).

3. PHASECHIP UN-PILL: the phase label is a flat tag — no border, no
   rounding, no padding. The Plan chip is the only button-shaped
   thing in the top bar. Drag-surface (pointer-events-none) and aria
   semantics unchanged; the transcript PhaseDivider keeps its look.

4. PANEL DEATH: components/graph-coverage.tsx, its graph-view
   mount, and tests/render-graph-coverage.test.tsx die whole.
   Confirmed: after this, NOTHING client-side consumes GET
   /api/graph/coverage — the only remaining client use of the
   coverage endpoints is the Extract POST (kept, by design); the
   routes stay (Platform's call).

Pure seams exported for tests: ringFill (clamp math),
CoverageMenuBody, CoverageRingGlyph, CoverageRingDetail.
901/901.

Live-verified on :3001: ring fill arcs match the API byte-for-byte
(483/4096 = 4.45/37.70; 1991/4096 = 18.32/37.70); the sidebar block
reads "2k uncovered / 4 messages unextracted"; a real Extract run
went optimistic → dock run → menu settled "fully extracted" AND the
ring drained to the empty arc (the feed chain); graph off → the ring
vanishes; the flat phase label renders chrome-free beside the Plan
pill.
2026-09-24 15:26:22 +03:00
amosblomqvist fb759b7aa0 fix: top-bar polish — lights on the pill line, rename escape hitbox, inert phase pill, chrome carve-outs
Traffic lights (owner report: 'way too low, not aligned with the phase pill'):
measured in the running app — circles centered at 30.75pt vs the pill box's
29.25pt (band 0-59.5, pill box 19-39.5, pill ink 28.75; uppercase glyphs sit
high). trafficLightPosition y 24 -> 22.5 lands the circle centers at 29.25,
exactly on the pill. Owner confirmed aligned after this change.

Rename escape hitbox (owner report): entering rename was already width-of-
title only, but the editing input spanned flex-1 so clicking the rest of the
bar stayed inside the input. The input is now shrink-to-fit
([field-sizing:content], min 8ch / max 70%) and any mousedown outside the
input blurs it, running the existing onBlur commit path; the header drops
lh-drag while renaming so the drag region cannot eat the escape click.
Verified: click-away commits (exactly one PUT), Escape discards, Enter
commits.

Phase pill is pure decoration (owner report: 'should not be interactable
in any way'): no tooltip, pointer-events-none, select-none — part of the
drag surface instead of a carved-out target.

Graph chrome was unclickable in Electron (owner report): home.tsx's
floating drag strip underlays the z-10 controls row, and Electron's
app-region mask ignores z-index, so the region ate their clicks (plain
browser unaffected — DOM hit-testing is fine). lh-no-drag carve-outs on
the chrome row and the node detail card (it is placed imperatively
anywhere, including the top strip).

LhMark home link now mirrors the ghost toggle: same size-9 chip geometry
and hover:bg-accent treatment.
2026-09-17 17:18:41 +03:00
amosblomqvist 9a6d422d59 feat: merged title bar — drag bands, owner's mark, title-width rename hit-box
The system title bar disappears (titleBarStyle hiddenInset +
trafficLightPosition {16,24}): the app's own 60px header bands become the
window's drag surface, and the traffic lights float over the sidebar's
mark band, vertically centered on the same line as the chat header's
phase pill (pixel-verified: circle centers y 24–36.5pt, pill box center
30pt).

- electron/main.cjs: hiddenInset chrome; dev shell (LH_DEV_URL) gets its
  own userData (-dev suffix) so it never fights the packaged app's
  single-instance lock — verified: the lock silently killed the dev
  shell while the packaged app was open (that also proved the lock works
  across separately-launched builds).
- app/globals.css: .lh-electron-gated utilities — drag/no-drag bands,
  92px traffic-light gutter (.lh-lights-gutter / .lh-closed-toggle:
  lights end ~75pt + the lights' own 16px edge inset as the gap),
  home's floating strip becomes a drag surface only in the shell.
  Everything is inert in the plain browser.
- sidebar.tsx: SidebarProvider sets <html class=lh-electron> post-mount
  (UA detection; no hydration mismatch, absent in browser); LH wordmark
  text replaced by the owner's mark (components/lh-mark.tsx, inlined
  icon-no-padding.svg, 24px, home link kept); SidebarToggle gains the
  92px margin when closed so it never slides under the static lights.
- chat.tsx: the rename hit-box shrinks to the title text itself —
  flex-1 spanned the whole bar, so every click started a rename and the
  window lost its grip. Shrink-to-fit + a flex-1 drag spacer (chips stay
  right) + hover:bg-accent affordance (clear when hover renames vs
  drags; -mx/px cancel for zero shift). The renaming input keeps flex-1
  for edit room.
- home.tsx / new-session.tsx / phase-divider.tsx: drag bands + carve-outs.

Live-verified in the dev shell (pixel analysis) and headless browser:
lights/pill/mark centered, 17pt lights→mark gap, rename click→select-all
→Escape roundtrip, browser layout untouched.

Gates: typecheck, eslint, 500/500 tests, build.
2026-09-17 16:55:07 +03:00
amosblomqvist c9ba4a1b06 Minimal-color round 2 (owner): the chip is label-only — spinner out of the top bar
The round-1 experiment put the braille spinner in the chip's old dot
slot; rejected live: the pill's curve and the square braille cell fought
for space (visually squeezed), and the chat surface already shows
in-flight work (thinking shimmer, the dock) — the chip stating the phase
is its whole job. PhaseChip back to a label-only pill (busy prop
removed, single-child gap dropped); work-state lives in exactly one
place: the sidebar row's spinner. Doc note + test pin the decision so
it doesn't get re-tried blind. Gates: typecheck, lint, 469/469, build.
2026-09-15 17:10:49 +03:00
amosblomqvist 326222f53c Minimal-color round 1 (owner): phase color retired — the braille spinner is the one working indicator
Nothing exists purely for decoration: phase color-coding is retired
app-wide (phases only advance — every session ends Teaching — so
phase-as-signal was decoration with a one-time function), and what
state genuinely deserved — 'a turn is in flight' — is now carried by
MOTION alone, in the terminal's own glyph language.

- BrailleSpinner (components/braille-spinner.tsx): the canonical
  cli-spinner sequence (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) as exact dot bitmaps in a 2×4
  braille cell, 80ms/frame (800ms cycle). Pure CSS: one shared keyframes,
  frames staggered via animation-delay; no JS timer, no re-render per
  frame — cheaper than the old breathing dot. Static contexts (SSR,
  prefers-reduced-motion) show frame 0 — a legible glyph, not a blank.
- Sidebar rows: spinner ONLY while a teach turn is in flight; idle and
  question-pending render NOTHING (a resting state has no scan value —
  the meta line's 'Waiting for your answer' text carries it). The
  three-state phase dot (pulse/solid/hollow) and PHASE_FILL/RING maps
  are gone.
- Top bar PhaseChip: label-only pill; the old dot slot becomes the
  spinner's home while a turn is in flight (busy prop wired from
  ChatView's busy = localStream || turnInFlight). Transcript
  PhaseDividers drop phase colors for monochrome rules + labels.
- API narrowing (app/api/chats/route.ts + ChatListItem contract):
  busy = isTurnRunning ONLY — background jobs no longer count (the
  sidebar's Background agents section is their channel; a per-row
  spinner would double-signal them).
- globals.css: --phase-* tokens + lh-dot-breathe removed (dead after
  the sweep — no remaining users).

Live-verified: idle/pending rows render no indicator, /chat/new chip is
label-only, 10 CSSAnimations at 800ms with correct 80ms stagger, static
frame 0 under reduced motion. Gates: typecheck, lint, 470/470, build.

Lane crossings to flag at merge: app/api/chats/route.ts + the
ChatListItem contract comment (src/core) — the busy-semantics narrowing;
components/chat.tsx one-line busy prop.
2026-09-15 17:01:01 +03:00
amosblomqvist a6d3883422 Wave-5 reskin batch 3: header title in CMU serif, markdown gets lh-prose
- Session title: font-title (CMU Serif bold) at text-2xl, per the mock.
- MessageResponse's markdown container now carries .lh-prose (Source
  Sans 3 + emphasis tokens) on every markdown surface; labels stay
  JetBrains Mono. Unlayered plain CSS beats the text-sm utility.
- PhaseChip + PhaseDivider sizes now pull from the declared text ladder
  (text-xs) instead of pixel literals.
2026-09-12 23:45:44 +03:00
Amos fbe4f16305 In-session phase presentation: header chip + dotted boundary dividers
- components/phase-divider.tsx: PhaseChip (header, next to the session
  title: phase-colored dot + Probing/Planning/Teaching) and PhaseDivider
  (the transcript boundary: color-coded dotted rules + a small chip —
  'Plan · N nodes' at the plan seam, 'Teaching' at the teach seam; probe
  never renders, phases only advance). planNodeCount reads the seam
  message's first plan tree.
- chat.tsx: dividers render at MESSAGE boundaries (phaseTimeline, one
  linear fold) — the seam is the transition to the message's phase, so
  the seam message's prose sits below its divider. requiresReveal now
  uses phaseAt3 teach (an occupied node), so probe- AND plan-phase
  questions auto-open.
- Retired the part-level plan-divider (artifact registry): the boundary
  is message-level, not part-level. Its tests replaced by
  render-phase-divider tests; core/phases gains tested phaseTimeline.
2026-09-01 18:31:01 +03:00