Work through TODO fixes
This commit is contained in:
@@ -326,6 +326,15 @@ func Run(ctx context.Context, opts Options) error {
|
||||
}
|
||||
}()
|
||||
|
||||
// Timer sidebar refresher: countdown labels are computed at draw
|
||||
// time, so wake the sidebar when the next visible timer bucket is
|
||||
// due to change even if no child PTY output arrives.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
st.runTimerSidebarRefresher(ctx)
|
||||
}()
|
||||
|
||||
// Marquee ticker: while a focused sidebar row's name overflows the
|
||||
// rail width, advance the pause-scroll-pause animation by marking
|
||||
// the sidebar dirty every marqueeStep. The chrome ticker above does
|
||||
@@ -671,6 +680,20 @@ func (st *uiState) clearViewportArea() {
|
||||
_, _ = os.Stdout.WriteString(b.String())
|
||||
}
|
||||
|
||||
func (st *uiState) repaintFocusedWithChrome() {
|
||||
st.mu.Lock()
|
||||
padFocused := st.focusedPad != ""
|
||||
st.mu.Unlock()
|
||||
if padFocused {
|
||||
st.repaintFocusedPad()
|
||||
} else {
|
||||
st.repaintFocused()
|
||||
}
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
}
|
||||
|
||||
func (st *uiState) restartFocusedCommand(processID string) {
|
||||
c := st.sess.FindChild(processID)
|
||||
if c == nil || c.Kind != KindCommand {
|
||||
@@ -687,14 +710,18 @@ func (st *uiState) restartFocusedCommand(processID string) {
|
||||
st.repaintNextPTYBudget = 2
|
||||
st.mu.Unlock()
|
||||
|
||||
st.outMu.Lock()
|
||||
_, _ = os.Stdout.Write(renderer.ClearViewport())
|
||||
st.outMu.Unlock()
|
||||
st.repaintFocusedWithChrome()
|
||||
|
||||
if err := st.sess.Restart(c.ID, syscall.SIGTERM, layout.childCols(), layout.childRows()); err != nil {
|
||||
st.flashError(fmt.Sprintf("restart %s: %v", c.DisplayName(), err))
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
return
|
||||
}
|
||||
st.outMu.Lock()
|
||||
_, _ = os.Stdout.Write(renderer.ClearViewport())
|
||||
st.outMu.Unlock()
|
||||
st.moveToViewportOrigin()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
@@ -741,12 +768,7 @@ func (st *uiState) notifyAttention(childID, reason string) {
|
||||
}
|
||||
|
||||
func (st *uiState) scratchpadsChanged() {
|
||||
st.padsCacheMu.Lock()
|
||||
st.padsCache = nil
|
||||
st.padsCacheMu.Unlock()
|
||||
st.chromeCacheMu.Lock()
|
||||
st.sidebarCache = ""
|
||||
st.chromeCacheMu.Unlock()
|
||||
st.invalidateScratchpadsCache()
|
||||
st.drawSidebar()
|
||||
st.mu.Lock()
|
||||
focusedPad := st.focusedPad
|
||||
@@ -756,6 +778,15 @@ func (st *uiState) scratchpadsChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
func (st *uiState) invalidateScratchpadsCache() {
|
||||
st.padsCacheMu.Lock()
|
||||
st.padsCache = nil
|
||||
st.padsCacheMu.Unlock()
|
||||
st.chromeCacheMu.Lock()
|
||||
st.sidebarCache = ""
|
||||
st.chromeCacheMu.Unlock()
|
||||
}
|
||||
|
||||
// OnChildSpawned auto-focuses the new child when the spawn came from
|
||||
// the user (palette, persistence restore, or an external MCP client with
|
||||
// no resolved identity). When ParentID is set — meaning a patterm-managed
|
||||
@@ -1143,6 +1174,55 @@ func (st *uiState) markSidebarDirty() {
|
||||
}
|
||||
}
|
||||
|
||||
func (st *uiState) runTimerSidebarRefresher(ctx context.Context) {
|
||||
if st.timers == nil {
|
||||
<-ctx.Done()
|
||||
return
|
||||
}
|
||||
changes := st.timers.changeEvents()
|
||||
var timer *time.Timer
|
||||
var timerC <-chan time.Time
|
||||
stop := func() {
|
||||
if timer == nil {
|
||||
return
|
||||
}
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
timer = nil
|
||||
timerC = nil
|
||||
}
|
||||
arm := func() {
|
||||
stop()
|
||||
wait, ok := st.timers.nextSidebarRefreshAfter(time.Now())
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if wait < timerSidebarMinRefresh {
|
||||
wait = timerSidebarMinRefresh
|
||||
}
|
||||
timer = time.NewTimer(wait)
|
||||
timerC = timer.C
|
||||
}
|
||||
defer stop()
|
||||
arm()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-changes:
|
||||
st.markSidebarDirty()
|
||||
arm()
|
||||
case <-timerC:
|
||||
st.markSidebarDirty()
|
||||
arm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (st *uiState) invalidateChromeCache() {
|
||||
st.chromeCacheMu.Lock()
|
||||
st.tabBarCache = ""
|
||||
@@ -1433,9 +1513,10 @@ func (st *uiState) processStdin(chunk []byte) {
|
||||
if st.focusedID != "" {
|
||||
if c := st.sess.FindChild(st.focusedID); c != nil && c.Status() == StatusRunning {
|
||||
prev := c.Owner()
|
||||
// InjectAsUser splits Enter bytes onto their own
|
||||
// writes so claude / codex / opencode don't treat a
|
||||
// "text\r" batch as a paste.
|
||||
// Agent panes split Enter bytes onto their own writes
|
||||
// so claude / codex / opencode don't treat a
|
||||
// "text\r" batch as a paste. Raw terminals keep paste
|
||||
// bytes batched.
|
||||
_ = c.InjectAsUser(forward)
|
||||
if st.summaries != nil {
|
||||
st.summaries.ObserveHumanInput(c.ID, forward)
|
||||
@@ -2131,20 +2212,47 @@ func (st *uiState) handlePadDelete(name string) {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
st.mu.Lock()
|
||||
wasFocused := st.focusedPad == name
|
||||
st.mu.Unlock()
|
||||
if err := st.pads.Delete(name); err != nil {
|
||||
st.flashError(fmt.Sprintf("delete %s: %v", name, err))
|
||||
return
|
||||
}
|
||||
st.mu.Lock()
|
||||
if st.focusedPad == name {
|
||||
if wasFocused {
|
||||
st.invalidateScratchpadsCache()
|
||||
if entries := st.padsList(); len(entries) > 0 {
|
||||
next := entries[0].Name
|
||||
st.mu.Lock()
|
||||
st.focusedPad = next
|
||||
st.focusedID = ""
|
||||
st.focusedName = next
|
||||
if st.padOffsetName != next {
|
||||
st.padOffset = 0
|
||||
st.padOffsetName = next
|
||||
}
|
||||
st.mu.Unlock()
|
||||
st.repaintFocusedWithChrome()
|
||||
return
|
||||
}
|
||||
if next := firstRunningTopLevel(st.sess.Children()); next != nil {
|
||||
st.focusProcess(next.ID)
|
||||
return
|
||||
}
|
||||
st.mu.Lock()
|
||||
st.focusedPad = ""
|
||||
st.focusedName = ""
|
||||
st.padOffset = 0
|
||||
st.padOffsetName = ""
|
||||
st.mu.Unlock()
|
||||
st.renderEmptyState()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
return
|
||||
}
|
||||
st.mu.Unlock()
|
||||
st.scratchpadsChanged()
|
||||
st.repaintFocused()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
st.repaintFocusedWithChrome()
|
||||
}
|
||||
|
||||
func (st *uiState) handlePadRename(oldName, newName string) {
|
||||
@@ -2293,8 +2401,19 @@ func (st *uiState) handleProcRestart(childID string) {
|
||||
return
|
||||
}
|
||||
layout := st.layoutSnapshot()
|
||||
st.mu.Lock()
|
||||
if c.ID == st.focusedID {
|
||||
st.renderer = newViewportRenderer(layout)
|
||||
st.repaintNextPTY = c.ID
|
||||
st.repaintNextPTYBudget = 2
|
||||
}
|
||||
st.mu.Unlock()
|
||||
st.repaintFocusedWithChrome()
|
||||
if err := st.sess.Restart(childID, syscall.SIGTERM, layout.childCols(), layout.childRows()); err != nil {
|
||||
st.flashError(fmt.Sprintf("restart %s: %v", c.DisplayName(), err))
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
return
|
||||
}
|
||||
st.repaintFocused()
|
||||
|
||||
Reference in New Issue
Block a user