perf: hoist keyMap constant, dedup selection toggles, split stdinHandler

- Move keyMap to module-level KEY_MAP constant (was recreated per keystroke)
- Extract toggleRowSelection() to replace 4x duplicated toggle logic
- Extract refreshMockSessions() to deduplicate demo setup in index.ts
- Split stdinHandler into processGridInput/processPickerInput
- Extract collapseProject helper from nested left/right key handling
- Add shared syntheticKey() and clearChildren() helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-28 02:14:53 +00:00
parent 6488816d89
commit 61add15531
3 changed files with 238 additions and 283 deletions

View File

@@ -10,6 +10,7 @@ import { loadGitMetadata } from "./data/git"
import { loadSessions } from "./data/sessions"
import { generateMockProjects, generateMockSessions, generateMockBusySessions } from "./data/mock"
import { detectActiveSessions, updateProjectSessions, generateMockActiveSessions, checkTransitions, snapshotBusy, playDoneSound, bounceDock, getSessionStatus, populateMockSessionStatus } from "./data/monitor"
import type { Project } from "./lib/types"
import { getUsageSummary } from "./data/usage"
import { getSessions, refreshAlive } from "./pty/session-manager"
import { stopAllCaptures } from "./pty/capture"
@@ -19,6 +20,18 @@ import { updateAll, rebuildDisplayRows, updateUsagePanel, updateColumnHeaders }
import { stdinHandler } from "./input/handlers"
import { resizeGridPanes } from "./grid/view-switch"
function refreshMockSessions(projects: Project[]) {
generateMockActiveSessions(projects)
generateMockBusySessions(projects)
for (const p of projects) {
if (p.activeSessions > 0 && !p.sessions) {
p.sessions = generateMockSessions(p.path)
p.sessionCount = p.sessions.length
}
populateMockSessionStatus(p)
}
}
async function main() {
process.stdout.write("\x1b[2J\x1b[H")
process.stdout.write("\x1b[1m cladm\x1b[0m\n")
@@ -165,15 +178,7 @@ async function main() {
// Live session monitoring
if (app.demoMode) {
generateMockActiveSessions(app.projects)
generateMockBusySessions(app.projects)
for (const p of app.projects) {
if (p.activeSessions > 0 && !p.sessions) {
p.sessions = generateMockSessions(p.path)
p.sessionCount = p.sessions.length
}
populateMockSessionStatus(p)
}
refreshMockSessions(app.projects)
app.prevBusySnapshot = snapshotBusy(app.projects)
updateAll()
} else {
@@ -197,15 +202,7 @@ async function main() {
if (app.demoMode) {
for (const p of app.projects) { p.activeSessions = 0; p.busySessions = 0 }
generateMockActiveSessions(app.projects)
generateMockBusySessions(app.projects)
for (const p of app.projects) {
if (p.activeSessions > 0 && !p.sessions) {
p.sessions = generateMockSessions(p.path)
p.sessionCount = p.sessions.length
}
populateMockSessionStatus(p)
}
refreshMockSessions(app.projects)
const transitioned = checkTransitions(app.projects, app.prevBusySnapshot)
app.prevBusySnapshot = snapshotBusy(app.projects)
if (transitioned.length > 0) {

View File

@@ -8,10 +8,12 @@ import { launchSelections } from "../actions/launcher"
import { loadSessions } from "../data/sessions"
import { loadBranches } from "../data/git"
import { generateMockSessions, generateMockBranches } from "../data/mock"
import { focusTerminalByPath, getSessionStatus, populateMockSessionStatus } from "../data/monitor"
import { focusTerminalByPath, populateMockSessionStatus } from "../data/monitor"
import { stopAllCaptures } from "../pty/capture"
import type { DisplayRow } from "../lib/types"
// ─── Constants ───────────────────────────────────────────────────────
// Shift+arrow sequences across terminal emulators
const SHIFT_ARROWS: Record<string, "up" | "down" | "left" | "right"> = {
"\x1b[1;2A": "up",
"\x1b[1;2B": "down",
@@ -23,6 +25,70 @@ const SHIFT_ARROWS: Record<string, "up" | "down" | "left" | "right"> = {
"\x1b[d": "left",
}
const KEY_MAP: Record<string, { name: string; shift?: boolean; ctrl?: boolean }> = {
"\x1b[A": { name: "up" },
"\x1b[B": { name: "down" },
"\x1b[C": { name: "right" },
"\x1b[D": { name: "left" },
"\x1b[5~": { name: "pageup" },
"\x1b[6~": { name: "pagedown" },
"\x1b[H": { name: "home" },
"\x1b[F": { name: "end" },
"\x1bOH": { name: "home" },
"\x1bOF": { name: "end" },
"\x1b[Z": { name: "tab", shift: true },
"\x1b[1;2A": { name: "up", shift: true },
"\x1b[1;2B": { name: "down", shift: true },
"\x1b[1;2C": { name: "right", shift: true },
"\x1b[1;2D": { name: "left", shift: true },
"\x09": { name: "tab" },
"\x0d": { name: "return" },
"\x1b": { name: "escape" },
" ": { name: "space" },
}
const NOOP = () => {}
// ─── Selection helpers ───────────────────────────────────────────────
function toggleSetItem<T>(set: Set<T>, item: T) {
if (set.has(item)) set.delete(item)
else set.add(item)
}
function toggleRowSelection(row: DisplayRow) {
const project = app.projects[row.projectIndex]
if (row.type === "project" || row.type === "new-session") {
toggleSetItem(app.selectedProjects, project.path)
} else if (row.type === "session") {
toggleSetItem(app.selectedSessions, project.sessions![row.sessionIndex!].id)
} else if (row.type === "branch") {
if (app.selectedBranches.get(project.path) === row.branchName) {
app.selectedBranches.delete(project.path)
} else {
app.selectedBranches.set(project.path, row.branchName!)
}
}
}
function syntheticKey(name: string, shift = false, ctrl = false): KeyEvent {
return { name, shift, ctrl, meta: false, preventDefault: NOOP, stopPropagation: NOOP } as KeyEvent
}
// ─── Collapse helper ─────────────────────────────────────────────────
function collapseProject(projectIndex: number) {
app.projects[projectIndex].expanded = false
rebuildDisplayRows()
const target = app.displayRows.findIndex(
(r) => r.type === "project" && r.projectIndex === projectIndex
)
app.cursor = target >= 0 ? target : 0
if (app.cursor >= app.displayRows.length) app.cursor = app.displayRows.length - 1
}
// ─── Expand ──────────────────────────────────────────────────────────
export async function expandProject(projectIndex: number) {
const project = app.projects[projectIndex]
if (app.demoMode) {
@@ -56,11 +122,11 @@ export async function expandProject(projectIndex: number) {
updateAll()
}
export function hitTestListRow(screenRow: number): number {
const listStartY = 2
const relY = screenRow - listStartY + app.listBox.scrollTop
if (relY < 0) return -1
// ─── Hit test ────────────────────────────────────────────────────────
export function hitTestListRow(screenRow: number): number {
const relY = screenRow - 2 + app.listBox.scrollTop
if (relY < 0) return -1
let y = 0
for (let i = 0; i < app.displayRows.length; i++) {
const h = app.displayRows[i].type === "session" ? 3 : 1
@@ -70,34 +136,18 @@ export function hitTestListRow(screenRow: number): number {
return -1
}
// ─── Picker click ────────────────────────────────────────────────────
export function handlePickerClick(_col: number, screenRow: number) {
const idx = hitTestListRow(screenRow)
if (idx < 0 || idx >= app.displayRows.length) return
app.cursor = idx
const row = app.displayRows[idx]
const project = app.projects[row.projectIndex]
if (row.type === "project" || row.type === "new-session") {
const path = project.path
if (app.selectedProjects.has(path)) app.selectedProjects.delete(path)
else app.selectedProjects.add(path)
} else if (row.type === "session") {
const session = project.sessions![row.sessionIndex!]
if (app.selectedSessions.has(session.id)) app.selectedSessions.delete(session.id)
else app.selectedSessions.add(session.id)
} else if (row.type === "branch") {
const path = project.path
if (app.selectedBranches.get(path) === row.branchName) {
app.selectedBranches.delete(path)
} else {
app.selectedBranches.set(path, row.branchName!)
}
}
toggleRowSelection(app.displayRows[idx])
updateAll()
}
// ─── Keyboard ────────────────────────────────────────────────────────
export async function handleKeypress(key: KeyEvent) {
try {
const total = app.displayRows.length
@@ -130,58 +180,23 @@ export async function handleKeypress(key: KeyEvent) {
case "right": {
const row = app.displayRows[app.cursor]
if (row.type === "project") {
const project = app.projects[row.projectIndex]
if (!project.expanded) {
expandProject(row.projectIndex)
return
}
if (row.type === "project" && !app.projects[row.projectIndex].expanded) {
expandProject(row.projectIndex)
return
}
return
}
case "left": {
const row = app.displayRows[app.cursor]
if (row.type === "project") {
app.projects[row.projectIndex].expanded = false
} else {
app.projects[row.projectIndex].expanded = false
const target = row.projectIndex
rebuildDisplayRows()
app.cursor = app.displayRows.findIndex(
(r) => r.type === "project" && r.projectIndex === target
)
if (app.cursor < 0) app.cursor = 0
}
rebuildDisplayRows()
if (app.cursor >= app.displayRows.length) app.cursor = app.displayRows.length - 1
case "left":
collapseProject(app.displayRows[app.cursor].projectIndex)
break
}
case "space": {
const row = app.displayRows[app.cursor]
if (row.type === "project" || row.type === "new-session") {
const path = app.projects[row.projectIndex].path
if (app.selectedProjects.has(path)) app.selectedProjects.delete(path)
else app.selectedProjects.add(path)
} else if (row.type === "session") {
const session = app.projects[row.projectIndex].sessions![row.sessionIndex!]
if (app.selectedSessions.has(session.id)) app.selectedSessions.delete(session.id)
else app.selectedSessions.add(session.id)
} else if (row.type === "branch") {
const path = app.projects[row.projectIndex].path
if (app.selectedBranches.get(path) === row.branchName) {
app.selectedBranches.delete(path)
} else {
app.selectedBranches.set(path, row.branchName!)
}
}
case "space":
toggleRowSelection(app.displayRows[app.cursor])
break
}
case "f": {
const row = app.displayRows[app.cursor]
const project = app.projects[row.projectIndex]
const project = app.projects[app.displayRows[app.cursor].projectIndex]
Bun.spawn(["open", project.path])
break
}
@@ -215,11 +230,10 @@ export async function handleKeypress(key: KeyEvent) {
case "tab":
if (app.bottomPanelMode === "idle" && app.cachedIdleSessions.length > 0) {
if (key.shift) {
app.idleCursor = app.idleCursor > 0 ? app.idleCursor - 1 : Math.min(app.cachedIdleSessions.length, 3) - 1
} else {
app.idleCursor = (app.idleCursor + 1) % Math.min(app.cachedIdleSessions.length, 3)
}
const max = Math.min(app.cachedIdleSessions.length, 3)
app.idleCursor = key.shift
? (app.idleCursor > 0 ? app.idleCursor - 1 : max - 1)
: (app.idleCursor + 1) % max
}
break
@@ -236,24 +250,18 @@ export async function handleKeypress(key: KeyEvent) {
break
}
if (app.bottomPanelMode === "idle" && app.cachedIdleSessions.length > 0 && app.idleCursor < app.cachedIdleSessions.length) {
const focused = await focusTerminalByPath(app.cachedIdleSessions[app.idleCursor].projectPath)
if (focused) return
if (await focusTerminalByPath(app.cachedIdleSessions[app.idleCursor].projectPath)) return
}
const returnRow = app.displayRows[app.cursor]
if (
returnRow.type === "project" &&
app.projects[returnRow.projectIndex].activeSessions > 0
) {
const focused = await focusTerminalByPath(app.projects[returnRow.projectIndex].path)
if (focused) return
if (returnRow.type === "project" && app.projects[returnRow.projectIndex].activeSessions > 0) {
if (await focusTerminalByPath(app.projects[returnRow.projectIndex].path)) return
}
doLaunch()
break
}
case "o": {
const hasOSel = app.selectedProjects.size > 0 || app.selectedSessions.size > 0
if (!hasOSel) {
if (app.selectedProjects.size === 0 && app.selectedSessions.size === 0) {
const oRow = app.displayRows[app.cursor]
if (oRow) app.selectedProjects.add(app.projects[oRow.projectIndex].path)
}
@@ -277,10 +285,7 @@ export async function handleKeypress(key: KeyEvent) {
return
case "t":
if (app.directGrid && app.directGrid.paneCount > 0) {
switchToGrid()
return
}
if (app.directGrid && app.directGrid.paneCount > 0) switchToGrid()
return
default:
@@ -291,6 +296,8 @@ export async function handleKeypress(key: KeyEvent) {
} catch {}
}
// ─── Grid input ──────────────────────────────────────────────────────
export async function handleGridInput(rawSequence: string): Promise<boolean> {
if (app.viewMode !== "grid" || !app.directGrid) return false
@@ -304,15 +311,8 @@ export async function handleGridInput(rawSequence: string): Promise<boolean> {
return true
}
if (rawSequence === "\x0e") {
app.directGrid.focusNext()
return true
}
if (rawSequence === "\x10") {
app.directGrid.focusPrev()
return true
}
if (rawSequence === "\x0e") { app.directGrid.focusNext(); return true }
if (rawSequence === "\x10") { app.directGrid.focusPrev(); return true }
if (rawSequence === "\x06") {
const pane = app.directGrid.focusedPane
@@ -327,26 +327,20 @@ export async function handleGridInput(rawSequence: string): Promise<boolean> {
const { killSession } = await import("../pty/session-manager")
app.directGrid.removePane(pane.session.name)
await killSession(pane.session.name)
if (app.directGrid.paneCount === 0) {
switchToPicker()
}
if (app.directGrid.paneCount === 0) switchToPicker()
}
return true
}
if (rawSequence === "\x1b[5~") {
app.directGrid.sendScrollToFocused("up")
return true
}
if (rawSequence === "\x1b[6~") {
app.directGrid.sendScrollToFocused("down")
return true
}
if (rawSequence === "\x1b[5~") { app.directGrid.sendScrollToFocused("up"); return true }
if (rawSequence === "\x1b[6~") { app.directGrid.sendScrollToFocused("down"); return true }
app.directGrid.sendInputToFocused(rawSequence)
return true
}
// ─── View switching ──────────────────────────────────────────────────
export function switchToPicker() {
app.viewMode = "picker"
if (app.directGrid) {
@@ -363,62 +357,46 @@ export function switchToPicker() {
app.renderer.requestRender()
}
export function stdinHandler(data: string | Buffer) {
const str = typeof data === "string" ? data : data.toString("utf8")
// ─── Stdin: grid mode ────────────────────────────────────────────────
if (app.viewMode === "grid" && app.directGrid) {
if (app.directGrid.selectMode) {
const keyboard = extractKeyboardInput(str)
if (keyboard === "\x1b") {
app.directGrid.exitSelectMode()
}
return
}
function processGridInput(str: string) {
const dg = app.directGrid!
const mouseEvents = extractMouseEvents(str)
for (const me of mouseEvents) {
if (me.btn === 64) {
app.directGrid.sendScrollToFocused("up", 3)
continue
}
if (me.btn === 65) {
app.directGrid.sendScrollToFocused("down", 3)
continue
}
if (me.btn === 0 && !me.release) {
const btn = app.directGrid.checkButtonClick(me.col, me.row)
if (btn?.action === "max") {
app.directGrid.expandPane(btn.paneIndex)
} else if (btn?.action === "min") {
app.directGrid.collapsePane()
} else if (btn?.action === "sel") {
app.directGrid.enterSelectMode()
} else {
app.directGrid.focusByClick(me.col, me.row)
}
continue
}
}
let stripped = str
for (let i = mouseEvents.length - 1; i >= 0; i--) {
const me = mouseEvents[i]
stripped = stripped.slice(0, me.start) + stripped.slice(me.end)
}
const keyboard = extractKeyboardInput(stripped)
if (keyboard) {
const dir = SHIFT_ARROWS[keyboard]
if (dir) {
app.directGrid.focusByDirection(dir)
} else {
handleGridInput(keyboard)
}
}
if (dg.selectMode) {
if (extractKeyboardInput(str) === "\x1b") dg.exitSelectMode()
return
}
// Picker mode
const mouseEvents = extractMouseEvents(str)
for (const me of mouseEvents) {
if (me.btn === 64) { dg.sendScrollToFocused("up", 3); continue }
if (me.btn === 65) { dg.sendScrollToFocused("down", 3); continue }
if (me.btn === 0 && !me.release) {
const btn = dg.checkButtonClick(me.col, me.row)
if (btn?.action === "max") dg.expandPane(btn.paneIndex)
else if (btn?.action === "min") dg.collapsePane()
else if (btn?.action === "sel") dg.enterSelectMode()
else dg.focusByClick(me.col, me.row)
continue
}
}
let stripped = str
for (let i = mouseEvents.length - 1; i >= 0; i--) {
const me = mouseEvents[i]
stripped = stripped.slice(0, me.start) + stripped.slice(me.end)
}
const keyboard = extractKeyboardInput(stripped)
if (!keyboard) return
const dir = SHIFT_ARROWS[keyboard]
if (dir) dg.focusByDirection(dir)
else handleGridInput(keyboard)
}
// ─── Stdin: picker mode ──────────────────────────────────────────────
function processPickerInput(str: string) {
const pickerMouse = extractMouseEvents(str)
for (const me of pickerMouse) {
if (me.btn === 0 && !me.release) handlePickerClick(me.col, me.row)
@@ -429,64 +407,32 @@ export function stdinHandler(data: string | Buffer) {
const keyboard = extractKeyboardInput(str)
if (!keyboard) return
const keyMap: Record<string, { name: string; shift?: boolean; ctrl?: boolean }> = {
"\x1b[A": { name: "up" },
"\x1b[B": { name: "down" },
"\x1b[C": { name: "right" },
"\x1b[D": { name: "left" },
"\x1b[5~": { name: "pageup" },
"\x1b[6~": { name: "pagedown" },
"\x1b[H": { name: "home" },
"\x1b[F": { name: "end" },
"\x1bOH": { name: "home" },
"\x1bOF": { name: "end" },
"\x1b[Z": { name: "tab", shift: true },
"\x1b[1;2A": { name: "up", shift: true },
"\x1b[1;2B": { name: "down", shift: true },
"\x1b[1;2C": { name: "right", shift: true },
"\x1b[1;2D": { name: "left", shift: true },
"\x09": { name: "tab" },
"\x0d": { name: "return" },
"\x1b": { name: "escape" },
" ": { name: "space" },
}
let ki = 0
while (ki < keyboard.length) {
let matched = false
for (let len = Math.min(8, keyboard.length - ki); len >= 1; len--) {
const seq = keyboard.slice(ki, ki + len)
const mapped = keyMap[seq]
const mapped = KEY_MAP[keyboard.slice(ki, ki + len)]
if (mapped) {
const syntheticKey = {
name: mapped.name,
shift: mapped.shift || false,
ctrl: mapped.ctrl || false,
meta: false,
preventDefault: () => {},
stopPropagation: () => {},
} as KeyEvent
handleKeypress(syntheticKey)
handleKeypress(syntheticKey(mapped.name, mapped.shift, mapped.ctrl))
ki += len
matched = true
break
}
}
if (!matched) {
const ch = keyboard[ki]
const code = ch.charCodeAt(0)
const code = keyboard.charCodeAt(ki)
if (code >= 0x21 && code <= 0x7e) {
const syntheticKey = {
name: ch,
shift: false,
ctrl: false,
meta: false,
preventDefault: () => {},
stopPropagation: () => {},
} as KeyEvent
handleKeypress(syntheticKey)
handleKeypress(syntheticKey(keyboard[ki]))
}
ki++
}
}
}
// ─── Stdin entry point ───────────────────────────────────────────────
export function stdinHandler(data: string | Buffer) {
const str = typeof data === "string" ? data : data.toString("utf8")
if (app.viewMode === "grid" && app.directGrid) processGridInput(str)
else processPickerInput(str)
}

View File

@@ -11,12 +11,14 @@ import {
magenta,
} from "@opentui/core"
import { app } from "../lib/state"
import { CURSOR_BG, ACTIVE_BG, ACCENT, DIM_CLR } from "../lib/theme"
import { CURSOR_BG, ACTIVE_BG, ACCENT } from "../lib/theme"
import { getSessionStatus, getIdleSessions } from "../data/monitor"
import { getUsageSummary, formatCost, formatWindow, makeBar, pct, PLAN_LIMITS } from "../data/usage"
import { formatCost, formatWindow, makeBar, pct, PLAN_LIMITS } from "../data/usage"
import { timeAgo, formatSize, elapsedCompact } from "../lib/time"
import { fmtProjectRow, fmtSessionRow, fmtNewSessionRow, fmtBranchRow, fmtSyncIndicator } from "./formatters"
// ─── Display rows ────────────────────────────────────────────────────
export function rebuildDisplayRows() {
app.displayRows = []
for (const idx of app.sortedIndices) {
@@ -65,6 +67,8 @@ export function applySortMode() {
rebuildDisplayRows()
}
// ─── Header / Footer ─────────────────────────────────────────────────
export function updateHeader() {
const total = app.selectedProjects.size + app.selectedSessions.size
const branchNote = app.selectedBranches.size > 0 ? ` (${app.selectedBranches.size} branch switch)` : ""
@@ -88,6 +92,20 @@ export function updateColumnHeaders() {
app.colHeaderText.content = t` ${dim(cols)}`
}
export function updateFooter() {
if (app.bottomPanelMode === "idle" && app.cachedIdleSessions.length > 0) {
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"
)}`
} else {
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"
)}`
}
}
// ─── Bottom panel ────────────────────────────────────────────────────
function addIdleRow(s: { idleSinceMs: number; projectName: string; sessionTitle: string; lastPrompt: string; lastResponse: string }, isCursor: boolean) {
const elapsed = (elapsedCompact(s.idleSinceMs) || "<5s").padEnd(6)
const name = s.projectName.length > 20 ? s.projectName.slice(0, 17) + "..." : s.projectName
@@ -104,11 +122,11 @@ function addIdleRow(s: { idleSinceMs: number; projectName: string; sessionTitle:
app.previewBox.add(Text({ content: t` ${dim("│")} ${dim("Claude:")} ${fg(ACCENT)('"' + response + '"')}`, width: "100%", height: 1 }))
}
export function updateIdlePanel() {
function updateIdlePanel() {
app.cachedIdleSessions = getIdleSessions(app.projects)
const n = app.cachedIdleSessions.length
app.previewBox.title = ` Idle Sessions (${n}) — enter to focus `
for (const child of app.previewBox.getChildren()) app.previewBox.remove(child.id)
clearChildren(app.previewBox)
if (n === 0) {
app.idleCursor = 0
app.previewBox.add(Text({ content: t`${dim(" No idle sessions")}`, width: "100%", height: 1 }))
@@ -129,7 +147,7 @@ export function updateBottomPanel() {
app.bottomRow.height = 14
updateIdlePanel()
} else {
for (const child of app.previewBox.getChildren()) app.previewBox.remove(child.id)
clearChildren(app.previewBox)
app.previewBox.add(app.previewText)
app.bottomRow.height = 10
app.previewBox.title = " Preview "
@@ -137,13 +155,15 @@ export function updateBottomPanel() {
}
}
// ─── Usage panel ─────────────────────────────────────────────────────
function usageBarColor(p: number) {
return p >= 80 ? yellow : p >= 50 ? cyan : green
}
export function updateUsagePanel() {
if (app.destroyed) return
for (const child of app.usageBox.getChildren()) app.usageBox.remove(child.id)
clearChildren(app.usageBox)
if (!app.cachedUsage) {
app.usageBox.title = " Usage "
@@ -179,17 +199,7 @@ export function updateUsagePanel() {
app.renderer.requestRender()
}
export function updateFooter() {
if (app.bottomPanelMode === "idle" && app.cachedIdleSessions.length > 0) {
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"
)}`
} else {
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"
)}`
}
}
// ─── Preview panel ───────────────────────────────────────────────────
export function updatePreview() {
if (app.cursor >= app.displayRows.length) {
@@ -236,53 +246,47 @@ ${selNote}`
}
}
export function rebuildList() {
for (const child of app.listBox.getChildren()) {
app.listBox.remove(child.id)
// ─── List rendering ──────────────────────────────────────────────────
// Renderable IDs for each row — enables incremental updates
let rowRenderableIds: string[] = []
function renderRowContent(i: number) {
const row = app.displayRows[i]
const project = app.projects[row.projectIndex]
let content: ReturnType<typeof t>
let rowHeight = 1
if (row.type === "project") {
content = fmtProjectRow(project, app.selectedProjects.has(project.path))
} else if (row.type === "session") {
content = fmtSessionRow(row.projectIndex, row.sessionIndex!, app.selectedSessions.has(project.sessions![row.sessionIndex!].id), false)
rowHeight = 3
} else if (row.type === "branch") {
content = fmtBranchRow(row.projectIndex, row.branchName!, app.selectedBranches.get(project.path) === row.branchName)
} else {
content = fmtNewSessionRow(row.projectIndex, app.selectedProjects.has(project.path))
}
const isCursor = i === app.cursor
const isActiveProject = row.type === "project" && project.activeSessions > 0
const isActiveSession = row.type === "session" && getSessionStatus(project.path, project.sessions![row.sessionIndex!].id) !== null
const bgColor = isCursor ? CURSOR_BG : (isActiveProject || isActiveSession) ? ACTIVE_BG : undefined
if (bgColor) {
return Box({ backgroundColor: bgColor, shouldFill: true, width: "100%", height: rowHeight }, Text({ content }))
}
return Text({ content, width: "100%", height: rowHeight })
}
export function rebuildList() {
clearChildren(app.listBox)
rowRenderableIds = []
for (let i = 0; i < app.displayRows.length; i++) {
const row = app.displayRows[i]
const isCursor = i === app.cursor
const project = app.projects[row.projectIndex]
let content: ReturnType<typeof t>
let rowHeight = 1
if (row.type === "project") {
const isSel = app.selectedProjects.has(project.path)
content = fmtProjectRow(project, isSel)
} else if (row.type === "session") {
const session = project.sessions![row.sessionIndex!]
const isSel = app.selectedSessions.has(session.id)
content = fmtSessionRow(row.projectIndex, row.sessionIndex!, isSel, false)
rowHeight = 3
} else if (row.type === "branch") {
const isSel = app.selectedBranches.get(project.path) === row.branchName
content = fmtBranchRow(row.projectIndex, row.branchName!, isSel)
} else {
const isSel = app.selectedProjects.has(project.path)
content = fmtNewSessionRow(row.projectIndex, isSel)
}
const isActiveProject = row.type === "project" && project.activeSessions > 0
const isActiveSession = row.type === "session" && getSessionStatus(project.path, project.sessions![row.sessionIndex!].id) !== null
const bgColor = isCursor ? CURSOR_BG : (isActiveProject || isActiveSession) ? ACTIVE_BG : undefined
if (bgColor) {
app.listBox.add(
Box(
{
backgroundColor: bgColor,
shouldFill: true,
width: "100%",
height: rowHeight,
},
Text({ content })
)
)
} else {
app.listBox.add(Text({ content, width: "100%", height: rowHeight }))
}
const vnode = renderRowContent(i)
const rid = app.listBox.add(vnode)
rowRenderableIds.push(rid as unknown as string)
}
ensureCursorVisible()
@@ -312,6 +316,14 @@ export function ensureCursorVisible() {
}
}
// ─── Helpers ─────────────────────────────────────────────────────────
function clearChildren(box: { getChildren(): { id: string }[]; remove(id: string): void }) {
for (const child of box.getChildren()) box.remove(child.id)
}
// ─── Top-level ───────────────────────────────────────────────────────
export function updateAll() {
if (app.destroyed) return
updateHeader()