Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 7 additions & 3 deletions native/cmd/lich-native/dock_slide.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions native/cmd/lich-native/dock_slide_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
24 changes: 18 additions & 6 deletions native/cmd/lich-native/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down
6 changes: 5 additions & 1 deletion native/cmd/lich-native/terminals.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Loading