feat: toggle between picker and grid without closing panes

Add pause()/resume() to DirectGridRenderer that detach/reattach frame
listeners without killing captures or PTY sessions. switchToPicker now
pauses the grid, switchToGrid resumes it. Panes keep running in the
background while browsing the project list.

Footer shows "t grid" hint when panes are active in the background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-28 09:56:38 +00:00
parent 61add15531
commit bc8a0c1934
4 changed files with 37 additions and 5 deletions

View File

@@ -89,6 +89,31 @@ export class DirectGridRenderer {
this.writeRaw(SHOW_CURSOR) this.writeRaw(SHOW_CURSOR)
} }
pause() {
this.running = false
if (this.titleTimer) { clearInterval(this.titleTimer); this.titleTimer = null }
// Detach frame listeners (stops rendering) but keep captures alive
for (const p of this.panes) p.directPane.detach()
}
resume() {
this.running = true
this.writeRaw(HIDE_CURSOR + CLEAR)
// Reattach frame listeners and redraw
for (let i = 0; i < this.panes.length; i++) {
const p = this.panes[i]
const dp = p.directPane
const idx = i
dp.attach(p.session.name)
dp.onFrame = (lines) => {
if (!this.running) return
this.drawPane(idx, lines)
}
}
this.repositionAll()
this.titleTimer = setInterval(() => this.refreshTitles(), 1000)
}
// ─── Getters ─────────────────────────────────────────── // ─── Getters ───────────────────────────────────────────
get focusIndex() { return this._focusIndex } get focusIndex() { return this._focusIndex }

View File

@@ -9,7 +9,8 @@ export function ensureGridView() {
export function switchToGrid() { export function switchToGrid() {
app.viewMode = "grid" app.viewMode = "grid"
if (!app.directGrid) { const isNew = !app.directGrid
if (isNew) {
app.directGrid = new DirectGridRenderer(app.rawStdoutWrite) app.directGrid = new DirectGridRenderer(app.rawStdoutWrite)
} }
@@ -20,7 +21,12 @@ export function switchToGrid() {
app.rawStdoutWrite("\x1b[?1049h") app.rawStdoutWrite("\x1b[?1049h")
app.rawStdoutWrite("\x1b[?1000h") app.rawStdoutWrite("\x1b[?1000h")
app.rawStdoutWrite("\x1b[?1006h") app.rawStdoutWrite("\x1b[?1006h")
if (isNew || app.directGrid.paneCount === 0) {
app.directGrid.start() app.directGrid.start()
} else {
app.directGrid.resume()
}
} }
export function resizeGridPanes() { export function resizeGridPanes() {

View File

@@ -345,7 +345,7 @@ export function switchToPicker() {
app.viewMode = "picker" app.viewMode = "picker"
if (app.directGrid) { if (app.directGrid) {
if (app.directGrid.selectMode) app.directGrid.exitSelectMode() if (app.directGrid.selectMode) app.directGrid.exitSelectMode()
if (app.directGrid.paneCount > 0) app.directGrid.stop() if (app.directGrid.paneCount > 0) app.directGrid.pause()
} }
app.renderer.resume() app.renderer.resume()
process.stdin.removeAllListeners("data") process.stdin.removeAllListeners("data")

View File

@@ -93,13 +93,14 @@ export function updateColumnHeaders() {
} }
export function updateFooter() { export function updateFooter() {
const gridHint = app.directGrid && app.directGrid.paneCount > 0 ? " │ t grid" : ""
if (app.bottomPanelMode === "idle" && app.cachedIdleSessions.length > 0) { if (app.bottomPanelMode === "idle" && app.cachedIdleSessions.length > 0) {
app.footerText.content = t` ${dim( app.footerText.content = t` ${dim(
"↑↓ nav │ tab/shift-tab idle select │ enter focus │ i preview │ space select │ a all │ n none │ s sort │ q quit" "↑↓ nav │ tab/shift-tab idle select │ enter focus │ i preview │ space select │ a all │ n none │ s sort │ q quit" + gridHint
)}` )}`
} else { } else {
app.footerText.content = t` ${dim( app.footerText.content = t` ${dim(
"↑↓ nav │ space select │ → expand │ ← collapse │ f folder │ g go to │ i idle │ a all │ n none │ s sort │ enter grid │ o external │ q quit" "↑↓ nav │ space select │ → expand │ ← collapse │ f folder │ g go to │ i idle │ a all │ n none │ s sort │ enter grid │ o external │ q quit" + gridHint
)}` )}`
} }
} }