From 06fbb437801ebb7e1924565981ab0b430ebf0d3d Mon Sep 17 00:00:00 2001 From: omartelo Date: Fri, 23 Oct 2026 11:24:10 -0300 Subject: [PATCH] fix(native): fit the terminal to the dock's destination as it starts sliding The terminal held its refit back for refitSettle after the dock stopped, so it sat clipped at the dock's edge and then jumped while the agent redrew. A lone session's terminal now takes the width the slide ends at on its first frame, and a split layout refits on the frame the dock lands. --- CHANGELOG.md | 1 + native/cmd/lich-native/dock_slide.go | 10 +++++++--- native/cmd/lich-native/dock_slide_test.go | 17 ++++++++++++++++ native/cmd/lich-native/main.go | 24 +++++++++++++++++------ native/cmd/lich-native/terminals.go | 6 +++++- 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c29b4b690..b61ffc118 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Kiro CLI sessions follow `KIRO_HOME`. With it set, lich read Kiro's conversations, cost and context from `~/.kiro` while Kiro kept them under `KIRO_HOME`, so they showed nothing and History listed none; the agent that carries lich's hooks was installed where Kiro never looked, and a sandboxed session was not given the directory it writes into. +- Opening or closing the dock no longer leaves the session's terminal cut off at its edge for a beat once the dock stops. The terminal takes its new width as the dock starts moving, so the agent redraws while the dock slides in, where the terminal waited until a tenth of a second after the dock stopped and then jumped. A split layout's terminals fit the moment the dock stops. ## [0.78.0] - 2026-10-22 diff --git a/native/cmd/lich-native/dock_slide.go b/native/cmd/lich-native/dock_slide.go index 5c659d9c6..4140a123b 100644 --- a/native/cmd/lich-native/dock_slide.go +++ b/native/cmd/lich-native/dock_slide.go @@ -8,20 +8,22 @@ import ( // dockSlide eases the dock in from the window's right edge and back out, on // the sidebar morph's 180ms curve. The dock keeps its width while it moves, -// so only the terminal beside it changes size, and that refits once it holds -// still. +// so only the island beside it changes size. type dockSlide struct { open bool from float32 // the share shown when the current slide began start time.Time shown float32 + // began and landed are whether this frame started or ended a slide. + began, landed bool } // at returns the share of the dock on screen this frame, 0 to 1, starting a // slide toward open when the target changed; a slide reversed halfway turns // back from where it is. func (s *dockSlide) at(gtx C, open bool) float32 { - if open != s.open { + s.began = open != s.open + if s.began { s.open, s.from, s.start = open, s.shown, gtx.Now } target := float32(0) @@ -30,9 +32,11 @@ func (s *dockSlide) at(gtx C, open bool) float32 { } t := float32(gtx.Now.Sub(s.start)) / float32(sidebarMorphDuration) if t >= 1 { + s.landed = s.shown != target s.shown = target return s.shown } + s.landed = false s.shown = s.from + (target-s.from)*sidebarMorphCurve(max(t, 0)) gtx.Execute(op.InvalidateCmd{}) return s.shown diff --git a/native/cmd/lich-native/dock_slide_test.go b/native/cmd/lich-native/dock_slide_test.go index 852691766..c536786f0 100644 --- a/native/cmd/lich-native/dock_slide_test.go +++ b/native/cmd/lich-native/dock_slide_test.go @@ -33,3 +33,20 @@ func TestDockSlideEasesInAndTurnsBackFromWhereItIs(t *testing.T) { t.Fatalf("a new opening starts closed: got %v, want 0", got) } } + +func TestDockSlideMarksTheFramesItBeginsAndEnds(t *testing.T) { + var s dockSlide + t0 := time.Unix(100, 0) + if slideAt(&s, t0, true); !s.began { + t.Fatal("first frame of an opening: not began, want began") + } + if slideAt(&s, t0.Add(sidebarMorphDuration/2), true); s.began || s.landed { + t.Fatalf("halfway: began %v landed %v, want neither", s.began, s.landed) + } + if slideAt(&s, t0.Add(sidebarMorphDuration), true); !s.landed { + t.Fatal("first frame at the end: not landed, want landed") + } + if slideAt(&s, t0.Add(2*sidebarMorphDuration), true); s.landed { + t.Fatal("a frame after the end: landed again, want once") + } +} diff --git a/native/cmd/lich-native/main.go b/native/cmd/lich-native/main.go index 85ade52ec..0934f5ed2 100644 --- a/native/cmd/lich-native/main.go +++ b/native/cmd/lich-native/main.go @@ -621,8 +621,10 @@ func (u *window) body(gtx C, v stageView) (int, sidebarAction, error) { // island is the stage's rounded box: Home, the empty notice, the diff or // the terminal, in the color the active tab merges with. Its top-left corner -// is square while that tab sits on it. -func (u *window) island(gtx C, v stageView) error { +// is square while that tab sits on it. A lone session's terminal is laid out +// termWidth wide, which a dock slide sets to where the box is going: wider, +// the box clips it; narrower, its fill shows past it. +func (u *window) island(gtx C, v stageView, termWidth int) error { size := gtx.Constraints.Max gtx.Constraints.Min = size r := gtx.Dp(ui.RadiusLG) @@ -653,7 +655,9 @@ func (u *window) island(gtx C, v stageView) error { } u.layoutStage.metric = gtx.Metric u.terms.onScreen(v.active.ID) - if err := u.content(gtx, v.active, u.terminalTakesKeys(gtx)); err != nil { + term := gtx + term.Constraints = layout.Exact(image.Pt(termWidth, size.Y)) + if err := u.content(term, v.active, u.terminalTakesKeys(gtx)); err != nil { return err } u.paneDropZones(gtx, v.active.ID, image.Rectangle{Max: size}, image.Rectangle{Max: size}.Add(u.islandAt), true) @@ -725,11 +729,19 @@ func (u *window) stageRow(gtx C, v stageView) error { d.slide.snap(open) } share := d.slide.at(gtx, open) + u.terms.refitNow = d.slide.began || d.slide.landed + defer func() { u.terms.refitNow = false }() + size := gtx.Constraints.Max + // The session's terminal takes the width the slide ends at from its first + // frame, so the agent redraws for it while the dock is still moving. + termWidth := size.X + if open && !d.fullscreen { + termWidth = max(size.X-d.width(gtx)-gtx.Dp(gutter), 0) + } if share == 0 { d.islands = nil - return u.island(gtx, v) + return u.island(gtx, v, termWidth) } - size := gtx.Constraints.Max at, w, slot := 0, size.X, 0 if !d.fullscreen { w = d.width(gtx) @@ -745,7 +757,7 @@ func (u *window) stageRow(gtx C, v stageView) error { } island := gtx island.Constraints = layout.Exact(image.Pt(max(size.X-slot, 0), size.Y)) - if err := u.island(island, v); err != nil { + if err := u.island(island, v, termWidth); err != nil { return err } defer clip.Rect{Max: size}.Push(gtx.Ops).Pop() diff --git a/native/cmd/lich-native/terminals.go b/native/cmd/lich-native/terminals.go index 54a49d014..f079e63a7 100644 --- a/native/cmd/lich-native/terminals.go +++ b/native/cmd/lich-native/terminals.go @@ -99,6 +99,10 @@ type terminals struct { // unzoomed is the window's own scale, which terminals draw at whatever // the interface zoom: a zoom step never changes a grid's cols×rows. unzoomed unit.Metric + // refitNow is a frame a dock slide began or ended in: the area just took + // the size it will hold, so the grid follows it at once rather than after + // refitSettle more of a clipped grid. + refitNow bool mu sync.Mutex byID map[string]*termView @@ -423,7 +427,7 @@ func sameCell(vt *gioterm.Terminal, w, h int) bool { // refit follows the area with the grid and the PTY once size has settled, // and until then asks for the frame that will. func (ts *terminals) refit(gtx layout.Context, t *termView, size image.Point, cols, rows int) error { - if due, at := t.refitDue(size, gtx.Now); !due { + if due, at := t.refitDue(size, gtx.Now); !due && !ts.refitNow { gtx.Execute(op.InvalidateCmd{At: at}) return nil }