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 @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Branch, changes and pull request on the cards and the footer catch up within a second of a commit, checkout or edit made outside a turn, where they could lag up to three seconds, and selecting a card reads its checkout at once instead of showing what the last read found.
- 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.
- Expanding the sidebar no longer leaves the session's terminal at its old width for a beat once the sidebar is open. The terminal takes its new width as the sidebar starts moving, as with the dock, where it waited until a tenth of a second after the sidebar stopped and then jumped; collapsing it refits at once too. A split layout's terminals fit the moment the sidebar stops.

## [0.78.0] - 2026-10-22

Expand Down
16 changes: 9 additions & 7 deletions native/cmd/lich-native/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,8 +622,8 @@ 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. 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.
// termWidth wide, which a dock slide or a sidebar morph 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
Expand Down Expand Up @@ -729,15 +729,17 @@ 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
u.terms.refitNow = d.slide.began || d.slide.landed || u.sidebar.morph.holdsStage()
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
// The session's terminal takes the width the dock's slide and the
// sidebar's morph end at from their first frame, so the agent redraws for
// it while they are still moving.
termWidth := size.X - u.sidebar.stageOverhang(gtx)
if open && !d.fullscreen {
termWidth = max(size.X-d.width(gtx)-gtx.Dp(gutter), 0)
termWidth -= d.width(gtx) + gtx.Dp(gutter)
}
termWidth = max(termWidth, 0)
if share == 0 {
d.islands = nil
return u.island(gtx, v, termWidth)
Expand Down
16 changes: 15 additions & 1 deletion native/cmd/lich-native/sidebar_morph.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ type sidebarMorph struct {
p float32 // this frame's eased progress
side morphSide
boxes [2]map[string]image.Rectangle // by side, in window coordinates

// landed is whether this frame ended the morph.
landed bool
}

// begin starts a morph to the shape the sidebar was just switched to.
Expand All @@ -73,10 +76,15 @@ func (m *sidebarMorph) begin(gtx C) {

func (m *sidebarMorph) active() bool { return m.phase != morphIdle }

// holdsStage is whether the stage's size this frame holds until the morph
// lands, or is the size it landed on.
func (m *sidebarMorph) holdsStage() bool { return m.active() || m.landed }

// advance sets the frame's progress. It runs once, before the frame is laid
// out, so every part of the frame reads the same one.
func (m *sidebarMorph) advance(gtx C) {
m.capturing = m.phase == morphCapture
m.landed = false
switch m.phase {
case morphIdle:
return
Expand All @@ -87,7 +95,7 @@ func (m *sidebarMorph) advance(gtx C) {
m.start = gtx.Now
}
if m.p = morphProgress(gtx.Now.Sub(m.start)); m.p >= 1 {
m.phase = morphIdle
m.phase, m.landed = morphIdle, true
return
}
}
Expand Down Expand Up @@ -183,6 +191,12 @@ func (s *sidebar) stageReserve(gtx C) int {
return min(s.boxWidth(gtx, true), s.boxWidth(gtx, false))
}

// stageOverhang is how much wider the stage is this frame than where the
// morph leaves it: the shapes' difference through an expand, else 0.
func (s *sidebar) stageOverhang(gtx C) int {
return s.boxWidth(gtx, s.open) - s.stageReserve(gtx)
}

// morphing is the sidebar mid-morph: its box eases between the two shapes'
// widths, each shape drawn at its own size from the box's leading corner and
// clipped by it, the old one fading out as the new one fades in.
Expand Down
44 changes: 44 additions & 0 deletions native/cmd/lich-native/sidebar_morph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,47 @@ func TestSidebarMorphResizesTheStageOnce(t *testing.T) {
}
}
}

// The terminal fits the width the morph ends at from its first frame, so the
// agent redraws while the sidebar is still moving: either way the terminal's
// width changes once, as the morph starts, to the final one, and the frames
// from there to the one the morph lands in refit it at once.
func TestSidebarMorphFitsTheTerminalToWhereItEndsAsItStarts(t *testing.T) {
const window = 1400
for _, opening := range []bool{true, false} {
s := newSidebar(new(ui.Root))
s.open = !opening
gtx := layout.Context{Ops: new(op.Ops), Metric: unit.Metric{PxPerDp: 1}, Now: time.Unix(0, 0)}
var widths []int
landings := 0
frame := func() {
s.morph.advance(gtx)
widths = append(widths, window-s.stageReserve(gtx)-s.stageOverhang(gtx))
if s.morph.landed {
landings++
}
if s.morph.active() && !s.morph.holdsStage() {
t.Fatalf("opening=%v: a morph frame does not hold the stage", opening)
}
s.morph.capture(gtx.Ops)
gtx.Now = gtx.Now.Add(testFrame)
}
frame()
s.setOpen(gtx, opening, new(sidebarAction))
for range 2 + int(sidebarMorphDuration/testFrame) + 2 {
frame()
}
after := window - s.boxWidth(gtx, opening)
for i, w := range widths[1:] {
if w != after {
t.Fatalf("opening=%v: frame %d terminal width %d, want %d from the morph's first frame", opening, i+1, w, after)
}
}
if landings != 1 {
t.Fatalf("opening=%v: landed in %d frames, want 1", opening, landings)
}
if s.morph.holdsStage() {
t.Fatalf("opening=%v: still holds the stage a frame after landing", opening)
}
}
}
1 change: 1 addition & 0 deletions native/cmd/lich-native/stale_chrome_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func stageFrames(t *testing.T, tab dockTab) (*window, *frameRig) {
r := newFrameRig(rigWindow, nil)
u := &window{model: newModel(nil, r.invalidate), terms: &terminals{byID: map[string]*termView{}, invalidate: r.invalidate}}
u.topbar = newTopbar(&u.root)
u.sidebar = newSidebar(&u.root)
_, client := newSidebarBackend(t)
comments := new(commentStore)
code, err := newCodeTab(client, r.invalidate, "monospace", 12, comments, func(ui.Toast) {})
Expand Down
6 changes: 3 additions & 3 deletions native/cmd/lich-native/terminals.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ 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 is a frame whose area holds its size until a dock slide or a
// sidebar morph lands, or the frame one landed in, so the grid follows it
// at once rather than after refitSettle more of a clipped grid.
refitNow bool

mu sync.Mutex
Expand Down
Loading