feat: inline pane tabs, add-pane mode, and grid chrome cleanup

- Tab bar now shows pane names inline with status icons instead of
  generic tab names with counts, eliminating the separate pane list row
- chromeTop reduced from 4 to 3, gaining one extra row of content space
- Add-pane mode (Ctrl+N) lets users add panes to existing tabs from picker
- Picker tab bar updated to match inline pane name format
- Session formatters and launch actions updated for branch switching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-28 18:13:25 +00:00
parent 9cf18f5740
commit e0f1a08098
7 changed files with 246 additions and 116 deletions

View File

@@ -4,6 +4,67 @@ import { ensureGridView, createNewGridTab, switchToGridTab } from "../grid/view-
import { loadSessions } from "../data/sessions" import { loadSessions } from "../data/sessions"
import { createSession } from "../pty/session-manager" import { createSession } from "../pty/session-manager"
export async function doAddPane() {
const targetTabId = app.addPaneTargetTabId
if (!targetTabId || !app.directGrid) return
if (app.selectedProjects.size === 0 && app.selectedSessions.size === 0) return
type LaunchItem = { path: string; name: string; sessionId?: string; targetBranch?: string }
const items: LaunchItem[] = []
for (const [path] of app.selectedProjects) {
const project = app.projects.find(p => p.path === path)
if (!project) continue
const targetBranch = app.selectedBranches.get(path)
const needsBranch = targetBranch && targetBranch !== project.branch
if (!project.sessions) {
project.sessions = await loadSessions(project.path)
project.sessionCount = project.sessions.length
}
const lastSessionId = project.sessions[0]?.id
items.push({ path, name: project.name, sessionId: lastSessionId, targetBranch: needsBranch ? targetBranch : undefined })
}
for (const project of app.projects) {
if (!project.sessions) continue
for (const session of project.sessions) {
if (app.selectedSessions.has(session.id)) {
const targetBranch = app.selectedBranches.get(project.path)
const needsBranch = targetBranch && targetBranch !== project.branch
items.push({ path: project.path, name: project.name, sessionId: session.id, targetBranch: needsBranch ? targetBranch : undefined })
}
}
}
if (items.length === 0) return
const termW = process.stdout.columns || 120
const termH = process.stdout.rows || 40
const totalPanes = items.length + (app.directGrid.getTabPaneCount(targetTabId) || 0)
const cols = totalPanes <= 1 ? 1 : totalPanes <= 2 ? 2 : totalPanes <= 4 ? 2 : 3
const rows = Math.ceil(totalPanes / cols)
const paneW = Math.max(Math.floor(termW / cols) - 2, 20)
const paneH = Math.max(Math.floor((termH - 2) / rows) - 4, 6)
for (const item of items) {
const session = await createSession({
projectPath: item.path,
projectName: item.name,
sessionId: item.sessionId,
targetBranch: item.targetBranch,
width: paneW,
height: paneH,
})
await app.directGrid.addPane(session, targetTabId)
}
app.selectedProjects.clear()
app.selectedSessions.clear()
app.selectedBranches.clear()
app.addPaneTargetTabId = null
switchToGridTab(targetTabId)
}
export async function doLaunch() { export async function doLaunch() {
if (app.selectedProjects.size === 0 && app.selectedSessions.size === 0) return if (app.selectedProjects.size === 0 && app.selectedSessions.size === 0) return
if (app.demoMode) { if (app.demoMode) {

View File

@@ -86,7 +86,7 @@ export class DirectGridRenderer {
private tabBarHitRegions: { tabId: number, startCol: number, endCol: number }[] = [] private tabBarHitRegions: { tabId: number, startCol: number, endCol: number }[] = []
private tabCloseHitRegions: { tabId: number, startCol: number, endCol: number }[] = [] private tabCloseHitRegions: { tabId: number, startCol: number, endCol: number }[] = []
private tabBarAddBtnCol = -1 private tabBarAddBtnCol = -1
// Pane list hit-test regions (row 2) // Pane name hit-test regions (inline in tab bar, row 1)
private paneListHitRegions: { tabId: number, paneIndex: number, startCol: number, endCol: number }[] = [] private paneListHitRegions: { tabId: number, paneIndex: number, startCol: number, endCol: number }[] = []
// Pending close state // Pending close state
@@ -371,7 +371,7 @@ export class DirectGridRenderer {
// Check if a click hit a button on the top border. Returns action + pane index. // Check if a click hit a button on the top border. Returns action + pane index.
// Hit areas are widened beyond the visible dot characters to make clicking easier. // Hit areas are widened beyond the visible dot characters to make clicking easier.
checkButtonClick(col: number, row: number): { action: "max" | "min" | "sel" | "tab" | "newtab" | "panefocus" | "closetab" | "closepane", paneIndex: number, tabId?: number } | null { checkButtonClick(col: number, row: number): { action: "max" | "min" | "sel" | "tab" | "newtab" | "panefocus" | "closetab" | "closepane", paneIndex: number, tabId?: number } | null {
// Tab bar check (row 1) // Tab bar check (row 1) — includes inline pane names
if (row === 1) { if (row === 1) {
// Check close buttons first — widened ±1 around the × character // Check close buttons first — widened ±1 around the × character
for (const region of this.tabCloseHitRegions) { for (const region of this.tabCloseHitRegions) {
@@ -379,6 +379,12 @@ export class DirectGridRenderer {
return { action: "closetab", paneIndex: -1, tabId: region.tabId } return { action: "closetab", paneIndex: -1, tabId: region.tabId }
} }
} }
// Pane names (inline in tabs) — check before tab regions since they're more specific
for (const region of this.paneListHitRegions) {
if (col >= region.startCol - 1 && col <= region.endCol + 1) {
return { action: "panefocus", paneIndex: region.paneIndex, tabId: region.tabId }
}
}
for (const region of this.tabBarHitRegions) { for (const region of this.tabBarHitRegions) {
if (col >= region.startCol && col <= region.endCol) { if (col >= region.startCol && col <= region.endCol) {
return { action: "tab", paneIndex: -1, tabId: region.tabId } return { action: "tab", paneIndex: -1, tabId: region.tabId }
@@ -390,15 +396,6 @@ export class DirectGridRenderer {
} }
return null return null
} }
// Pane list check (row 2) — widened ±1 for easier clicks
if (row === 2) {
for (const region of this.paneListHitRegions) {
if (col >= region.startCol - 1 && col <= region.endCol + 1) {
return { action: "panefocus", paneIndex: region.paneIndex, tabId: region.tabId }
}
}
return null
}
const indicesToCheck = this.isExpanded ? [this._expandedIndex] : this.panes.map((_, i) => i) const indicesToCheck = this.isExpanded ? [this._expandedIndex] : this.panes.map((_, i) => i)
for (const i of indicesToCheck) { for (const i of indicesToCheck) {
@@ -562,7 +559,7 @@ export class DirectGridRenderer {
if (n === 0) return false if (n === 0) return false
const termW = process.stdout.columns || 120 const termW = process.stdout.columns || 120
const termH = process.stdout.rows || 40 const termH = process.stdout.rows || 40
const chromeTop = 4 const chromeTop = 3
const { cols } = this.calcGrid(n) const { cols } = this.calcGrid(n)
const rows = Math.ceil(n / cols) const rows = Math.ceil(n / cols)
const cellW = Math.floor(termW / cols) const cellW = Math.floor(termW / cols)
@@ -594,7 +591,7 @@ export class DirectGridRenderer {
if (n === 0) return -1 if (n === 0) return -1
const termW = process.stdout.columns || 120 const termW = process.stdout.columns || 120
const termH = process.stdout.rows || 40 const termH = process.stdout.rows || 40
const chromeTop = 4 const chromeTop = 3
const { cols } = this.calcGrid(n) const { cols } = this.calcGrid(n)
const rows = Math.ceil(n / cols) const rows = Math.ceil(n / cols)
const cellW = Math.floor(termW / cols) const cellW = Math.floor(termW / cols)
@@ -614,13 +611,10 @@ export class DirectGridRenderer {
let out = SYNC_START let out = SYNC_START
// Tab bar (row 1) // Tab bar (row 1) — includes inline pane names
out += this.drawTabBar(termW) out += this.drawTabBar(termW)
// Pane list (row 2) // Header (row 2)
out += this.drawPaneList(termW)
// Header (row 3)
const n = this.panes.length const n = this.panes.length
const fi = this._focusIndex + 1 const fi = this._focusIndex + 1
let headerLeft: string, headerRight: string let headerLeft: string, headerRight: string
@@ -637,7 +631,7 @@ export class DirectGridRenderer {
headerLeft = ` ${BOLD}cladm grid${RESET}${n} sessions │ focus: ${fi}/${n}` headerLeft = ` ${BOLD}cladm grid${RESET}${n} sessions │ focus: ${fi}/${n}`
headerRight = `${DIM}shift+arrows nav │ ${hexFg("#f7768e")}[●]${RESET}${DIM} close ${hexFg("#9ece6a")}[●]${RESET}${DIM} expand │ ctrl+s select │ ctrl+space picker${RESET}` headerRight = `${DIM}shift+arrows nav │ ${hexFg("#f7768e")}[●]${RESET}${DIM} close ${hexFg("#9ece6a")}[●]${RESET}${DIM} expand │ ctrl+s select │ ctrl+space picker${RESET}`
} }
out += `\x1b[3;1H\x1b[${termW}X${headerLeft} ${headerRight}` out += `\x1b[2;1H\x1b[${termW}X${headerLeft} ${headerRight}`
// Pane borders + titles // Pane borders + titles
if (this.isExpanded) { if (this.isExpanded) {
@@ -671,6 +665,7 @@ export class DirectGridRenderer {
private drawTabBar(termW: number): string { private drawTabBar(termW: number): string {
this.tabBarHitRegions = [] this.tabBarHitRegions = []
this.tabCloseHitRegions = [] this.tabCloseHitRegions = []
this.paneListHitRegions = []
this.tabBarAddBtnCol = -1 this.tabBarAddBtnCol = -1
const RED_FG = hexFg("#f7768e") const RED_FG = hexFg("#f7768e")
@@ -688,49 +683,106 @@ export class DirectGridRenderer {
out += ` ${DIM}○ Picker${RESET} ` out += ` ${DIM}○ Picker${RESET} `
} }
const pickerStart = pickerActive ? col + 1 : col + 1 const pickerStart = pickerActive ? col + 1 : col + 1
const pickerVisLen = pickerActive ? 10 : 10
this.tabBarHitRegions.push({ tabId: -1, startCol: pickerStart, endCol: pickerStart + 7 }) this.tabBarHitRegions.push({ tabId: -1, startCol: pickerStart, endCol: pickerStart + 7 })
col += pickerVisLen col += 10
// Grid tabs // Grid tabs — inline pane names instead of tab names
for (const tab of app.gridTabs) { for (const tab of app.gridTabs) {
const isActive = this._activeTabId === tab.id && app.viewMode === "grid" const isActive = this._activeTabId === tab.id && app.viewMode === "grid"
const hasIdle = this.hasIdleInTab(tab.id)
const count = this.getTabPaneCount(tab.id)
const label = `${tab.name} (${count})`
const isPending = this._pendingCloseTabId === tab.id const isPending = this._pendingCloseTabId === tab.id
const tabPanes = this.tabPanes.get(tab.id) ?? []
const startCol = col + (isActive ? 2 : 1) // account for ╭ + space or just space // Build pane name list for this tab
const visLen = 2 + label.length // "● " + label const paneLabels: { name: string, color: string, status: PaneStatus, isFocused: boolean }[] = []
for (let pi = 0; pi < tabPanes.length; pi++) {
const p = tabPanes[pi]!
const name = p.session.projectName
const short = name.length > 14 ? name.slice(0, 12) + "…" : name
paneLabels.push({
name: short,
color: getColor(p.session.colorIndex),
status: p.status,
isFocused: isActive && this._focusIndex === pi,
})
}
// Close button text — framed for visibility // Close button text
const closeText = isPending ? `${RED_FG}${BOLD}[●]${RESET}` : `${DIM}[×]${RESET}` const closeText = isPending ? `${RED_FG}${BOLD}[●]${RESET}` : `${DIM}[×]${RESET}`
const closeVisLen = 3 const closeVisLen = 3
const tabStartCol = col
if (isActive) { if (isActive) {
// Chrome-style raised active tab // Active tab: ╭ ● pane1 · ◉ pane2 ×
out += `${TAB_BORDER}${RESET}${TAB_BG_ACTIVE} ${CYAN_FG}${BOLD}${label}${RESET}${TAB_BG_ACTIVE} ${closeText}${TAB_BG_ACTIVE} ${RESET}${TAB_BORDER}${RESET}` out += `${TAB_BORDER}${RESET}${TAB_BG_ACTIVE} `
// ╭ + space + ● label + space + × + space + ╮ col += 2 // ╭ + space
const totalVis = 1 + 1 + visLen + 1 + closeVisLen + 1 + 1
this.tabBarHitRegions.push({ tabId: tab.id, startCol, endCol: startCol + visLen - 1 }) for (let pi = 0; pi < paneLabels.length; pi++) {
const closeStartCol = startCol + visLen + 1 const pl = paneLabels[pi]!
this.tabCloseHitRegions.push({ tabId: tab.id, startCol: closeStartCol, endCol: closeStartCol + closeVisLen - 1 }) let icon: string
col += totalVis if (pl.status === "busy") icon = `${hexFg("#9ece6a")}${RESET}`
} else { else if (pl.status === "idle") icon = `${hexFg("#e0af68")}${RESET}`
// Inactive tab — flat, no border else icon = `${DIM}${RESET}`
let indicator: string
if (hasIdle) { const paneStartCol = col
indicator = `${YELLOW_FG}${label}${RESET}` if (pl.isFocused) {
} else { out += `${TAB_BG_ACTIVE}${icon} ${hexFg(pl.color)}${BOLD}${pl.name}${RESET}`
indicator = `${DIM}${label}${RESET}` } else {
out += `${TAB_BG_ACTIVE}${icon} ${DIM}${pl.name}${RESET}`
}
col += 2 + pl.name.length // icon + space + name
this.paneListHitRegions.push({ tabId: tab.id, paneIndex: pi, startCol: paneStartCol, endCol: col - 1 })
if (pi < paneLabels.length - 1) {
out += `${TAB_BG_ACTIVE}${DIM} · ${RESET}`
col += 3
}
} }
out += ` ${indicator} ${closeText} ${DIM}${RESET}`
// space + ● label + space + × + space + │ if (paneLabels.length === 0) {
const totalVis = 1 + visLen + 1 + closeVisLen + 1 + 1 out += `${TAB_BG_ACTIVE}${DIM}empty${RESET}`
this.tabBarHitRegions.push({ tabId: tab.id, startCol, endCol: startCol + visLen - 1 }) col += 5
const closeStartCol = startCol + visLen + 1 }
out += `${TAB_BG_ACTIVE} ${closeText}${TAB_BG_ACTIVE} ${RESET}${TAB_BORDER}${RESET}`
const closeStartCol = col + 1
col += 1 + closeVisLen + 1 + 1 // space + [×] + space + ╮
this.tabCloseHitRegions.push({ tabId: tab.id, startCol: closeStartCol, endCol: closeStartCol + closeVisLen - 1 }) this.tabCloseHitRegions.push({ tabId: tab.id, startCol: closeStartCol, endCol: closeStartCol + closeVisLen - 1 })
col += totalVis this.tabBarHitRegions.push({ tabId: tab.id, startCol: tabStartCol, endCol: col - 1 })
} else {
// Inactive tab: ○ pane1 · pane2 ×
const hasIdle = this.hasIdleInTab(tab.id)
out += ` `
col += 1
for (let pi = 0; pi < paneLabels.length; pi++) {
const pl = paneLabels[pi]!
let icon: string
if (pl.status === "idle") icon = `${YELLOW_FG}${RESET}`
else if (pl.status === "busy") icon = `${DIM}${RESET}`
else icon = `${DIM}${RESET}`
const paneStartCol = col
out += `${icon} ${DIM}${pl.name}${RESET}`
col += 2 + pl.name.length
this.paneListHitRegions.push({ tabId: tab.id, paneIndex: pi, startCol: paneStartCol, endCol: col - 1 })
if (pi < paneLabels.length - 1) {
out += `${DIM} · ${RESET}`
col += 3
}
}
if (paneLabels.length === 0) {
out += `${DIM}empty${RESET}`
col += 5
}
out += ` ${closeText} ${DIM}${RESET}`
const closeStartCol = col + 1
col += 1 + closeVisLen + 1 + 1 // space + [×] + space + │
this.tabCloseHitRegions.push({ tabId: tab.id, startCol: closeStartCol, endCol: closeStartCol + closeVisLen - 1 })
this.tabBarHitRegions.push({ tabId: tab.id, startCol: tabStartCol, endCol: col - 1 })
} }
} }
@@ -743,50 +795,7 @@ export class DirectGridRenderer {
return out return out
} }
private drawPaneList(termW: number): string {
this.paneListHitRegions = []
let out = `\x1b[2;1H\x1b[${termW}X `
let col = 3
// Show panes across all tabs, grouped by tab
for (const tab of app.gridTabs) {
const tabPanes = this.tabPanes.get(tab.id) ?? []
if (tabPanes.length === 0) continue
for (let pi = 0; pi < tabPanes.length; pi++) {
const pane = tabPanes[pi]!
const isFocused = this._activeTabId === tab.id && this._focusIndex === pi
const name = pane.session.projectName
const short = name.length > 14 ? name.slice(0, 12) + "…" : name
const color = getColor(pane.session.colorIndex)
// Status icon: ● green=running, ◉ yellow=idle, ○ dim=unknown
let statusIcon: string
if (pane.status === "busy") statusIcon = `${hexFg("#9ece6a")}${RESET}`
else if (pane.status === "idle") statusIcon = `${hexFg("#e0af68")}${RESET}`
else statusIcon = `${DIM}${RESET}`
const startCol = col
if (isFocused) {
out += `${statusIcon}${hexFg(color)}${BOLD}${short}${RESET}`
} else {
out += `${statusIcon}${DIM}${short}${RESET}`
}
col += 2 + short.length // icon + space + name
this.paneListHitRegions.push({ tabId: tab.id, paneIndex: pi, startCol, endCol: col - 1 })
if (pi < tabPanes.length - 1) {
out += `${DIM} · ${RESET}`
col += 3
}
}
out += `${DIM}${RESET}`
col += 5
}
return out
}
private drawPaneBorder(index: number): string { private drawPaneBorder(index: number): string {
const pane = this.panes[index]! const pane = this.panes[index]!
@@ -973,7 +982,7 @@ export class DirectGridRenderer {
const n = count ?? this.panes.length const n = count ?? this.panes.length
const termW = process.stdout.columns || 120 const termW = process.stdout.columns || 120
const termH = process.stdout.rows || 40 const termH = process.stdout.rows || 40
const chromeTop = 4 // row 1 = tab bar, row 2 = pane list, row 3 = header, content starts row 4 const chromeTop = 3 // row 1 = tab bar (with inline panes), row 2 = header, content starts row 3
const { cols, rows } = this.calcGrid(n) const { cols, rows } = this.calcGrid(n)
const cellW = Math.floor(termW / cols) const cellW = Math.floor(termW / cols)
const cellH = Math.floor((termH - chromeTop - 1) / rows) // -1 for footer const cellH = Math.floor((termH - chromeTop - 1) / rows) // -1 for footer
@@ -999,7 +1008,7 @@ export class DirectGridRenderer {
repositionAll() { repositionAll() {
const termW = process.stdout.columns || 120 const termW = process.stdout.columns || 120
const termH = process.stdout.rows || 40 const termH = process.stdout.rows || 40
const chromeTop = 4 const chromeTop = 3
if (this.isExpanded) { if (this.isExpanded) {
// Fullscreen: expanded pane gets all space // Fullscreen: expanded pane gets all space

View File

@@ -72,7 +72,7 @@ async function main() {
try { try {
const state = extractSessionState() const state = extractSessionState()
if (state) saveSessionSync(state) if (state) saveSessionSync(state)
} catch {} } catch (err) { console.error("[session-save]", err) }
if (app.monitorInterval) { clearInterval(app.monitorInterval); app.monitorInterval = null } if (app.monitorInterval) { clearInterval(app.monitorInterval); app.monitorInterval = null }
if (app.directGrid) app.directGrid.destroyAll() if (app.directGrid) app.directGrid.destroyAll()
stopAllCaptures() stopAllCaptures()
@@ -187,7 +187,7 @@ async function main() {
getUsageSummary().then(u => { getUsageSummary().then(u => {
app.cachedUsage = u app.cachedUsage = u
updateUsagePanel() updateUsagePanel()
}).catch(() => {}) }).catch(err => console.error("[usage]", err))
// Resize PTY panes when terminal window is resized // Resize PTY panes when terminal window is resized
process.stdout.on("resize", () => { process.stdout.on("resize", () => {
@@ -220,7 +220,7 @@ async function main() {
try { try {
app.cachedUsage = await getUsageSummary() app.cachedUsage = await getUsageSummary()
updateUsagePanel() updateUsagePanel()
} catch {} } catch (err) { console.error("[usage-poll]", err) }
} }
if (app.demoMode) { if (app.demoMode) {

View File

@@ -225,19 +225,25 @@ function handlePickerTabBarClick(col: number, screenRow: number) {
c = 11 c = 11
for (const tab of app.gridTabs) { for (const tab of app.gridTabs) {
const count = app.directGrid?.getTabPaneCount(tab.id) ?? 0
const isActive = app.viewMode === "grid" && app.directGrid?.activeTabId === tab.id const isActive = app.viewMode === "grid" && app.directGrid?.activeTabId === tab.id
const label = `${tab.name} (${count})`
const visLen = 2 + label.length // "● " + label // Build inline pane name list to calculate width
const tabPanes = app.directGrid?.getTabPanes(tab.id) ?? []
const paneNames = tabPanes.map(p => {
const name = p.session.projectName
return name.length > 14 ? name.slice(0, 12) + "…" : name
})
const inlineLabel = paneNames.length > 0 ? paneNames.join(" · ") : "empty"
const visLen = 2 + inlineLabel.length // "● " + label
const dg = app.directGrid const dg = app.directGrid
if (isActive) { if (isActive) {
// Active: ╭ ● label × ╮ → c+1=╭, c+2=space, c+3..=● label, then close, // Active: ╭ ● panes ×
const labelStart = c + 2 const labelStart = c + 2
const labelEnd = labelStart + visLen - 1 const labelEnd = labelStart + visLen - 1
const closeCol = labelEnd + 2 // space + × position const closeCol = labelEnd + 2
const totalVis = 1 + 1 + visLen + 1 + 1 + 1 + 1 // ╭ + sp + label + sp + × + sp + ╮ const totalVis = 1 + 1 + visLen + 1 + 1 + 1 + 1
if (col === closeCol && dg) { if (col === closeCol && dg) {
const result = dg.requestCloseTab(tab.id) const result = dg.requestCloseTab(tab.id)
@@ -251,11 +257,11 @@ function handlePickerTabBarClick(col: number, screenRow: number) {
} }
c += totalVis c += totalVis
} else { } else {
// Inactive: sp ● label sp × sp │ → 1 + visLen + 1 + 1 + 1 + 1 // Inactive: sp ● panes sp × sp │
const labelStart = c + 1 const labelStart = c + 1
const labelEnd = labelStart + visLen - 1 const labelEnd = labelStart + visLen - 1
const closeCol = labelEnd + 2 const closeCol = labelEnd + 2
const totalVis = 1 + visLen + 1 + 1 + 1 + 1 // sp + label + sp + × + sp + │ const totalVis = 1 + visLen + 1 + 1 + 1 + 1
if (col === closeCol && dg) { if (col === closeCol && dg) {
const result = dg.requestCloseTab(tab.id) const result = dg.requestCloseTab(tab.id)

View File

@@ -49,6 +49,7 @@ export const app = {
lastGridTabIndex: 0, // last active grid tab for Ctrl+Space toggle lastGridTabIndex: 0, // last active grid tab for Ctrl+Space toggle
savedSession: null as SavedSession | null, savedSession: null as SavedSession | null,
restoreMode: null as "pending" | null, restoreMode: null as "pending" | null,
addPaneTargetTabId: null as number | null,
// UI refs (set during init) // UI refs (set during init)
renderer: null as unknown as CliRenderer, renderer: null as unknown as CliRenderer,

View File

@@ -22,7 +22,7 @@ export function fmtSyncIndicator(ahead: number, behind: number): string {
return parts.join("") return parts.join("")
} }
const TAB_COLORS = [ export const TAB_COLORS = [
cyan, // 1 cyan, // 1
green, // 2 green, // 2
yellow, // 3 yellow, // 3
@@ -34,6 +34,33 @@ const TAB_COLORS = [
(s: string) => fg("#b4f9f8")(s), // 9 (s: string) => fg("#b4f9f8")(s), // 9
] ]
function getGridTabBadges(projectPath: string): string {
if (!app.directGrid || app.gridTabs.length === 0) return ""
const badges: string[] = []
for (const tab of app.gridTabs) {
const panes = app.directGrid.getTabPanes(tab.id)
if (panes.some(p => p.session.projectPath === projectPath)) {
const displayIdx = app.gridTabs.indexOf(tab) + 1
const color = TAB_COLORS[(displayIdx - 1) % TAB_COLORS.length]!
badges.push(color(`T${displayIdx}`))
}
}
return badges.length > 0 ? badges.join("") + " " : ""
}
function getSessionGridTabBadge(projectPath: string, sessionId: string): string {
if (!app.directGrid || app.gridTabs.length === 0) return ""
for (const tab of app.gridTabs) {
const panes = app.directGrid.getTabPanes(tab.id)
if (panes.some(p => p.session.projectPath === projectPath && p.session.sessionId === sessionId)) {
const displayIdx = app.gridTabs.indexOf(tab) + 1
const color = TAB_COLORS[(displayIdx - 1) % TAB_COLORS.length]!
return " " + color(`T${displayIdx}`)
}
}
return ""
}
function fmtTabCheck(tabNum: number | undefined) { function fmtTabCheck(tabNum: number | undefined) {
if (tabNum === undefined) return " " if (tabNum === undefined) return " "
const color = TAB_COLORS[(tabNum - 1) % TAB_COLORS.length]! const color = TAB_COLORS[(tabNum - 1) % TAB_COLORS.length]!
@@ -83,7 +110,8 @@ export function fmtProjectRow(project: import("../lib/types").Project, isSelecte
else if (ca.includes("d ago")) claudeCol = green(ca.padEnd(9)) else if (ca.includes("d ago")) claudeCol = green(ca.padEnd(9))
else claudeCol = dim(ca.padEnd(9)) else claudeCol = dim(ca.padEnd(9))
return t` ${activeDot}${activeTag}[${check}] ${dim(arrow)} ${name.padEnd(28)} ${magenta(branch.padEnd(9))}${syncCol}${dim( const gridBadge = getGridTabBadges(project.path)
return t` ${activeDot}${activeTag}${gridBadge}[${check}] ${dim(arrow)} ${name.padEnd(28)} ${magenta(branch.padEnd(9))}${syncCol}${dim(
(project.commitAge || "-").padEnd(10) (project.commitAge || "-").padEnd(10)
)}${(project.commitMsg || "-").padEnd(22)}${dirtyCol}${claudeCol}${dim( )}${(project.commitMsg || "-").padEnd(22)}${dirtyCol}${claudeCol}${dim(
String(project.sessionCount).padStart(3) String(project.sessionCount).padStart(3)
@@ -120,17 +148,19 @@ export function fmtSessionRow(
: session.lastAssistantMsg : session.lastAssistantMsg
: "(no text response)" : "(no text response)"
const tabBadge = session.id ? getSessionGridTabBadge(project.path, session.id) : ""
if (status === "busy") { if (status === "busy") {
return t` ${green("●")} ${dim(prefix)} [${check}] ${dim(age.padEnd(9))} ${dim( return t` ${green("●")} ${dim(prefix)} [${check}] ${dim(age.padEnd(9))} ${dim(
size.padEnd(7) size.padEnd(7)
)} ${fg(ACCENT)('"' + title + '"')} ${green("running")} )} ${fg(ACCENT)('"' + title + '"')} ${green("running")}${tabBadge}
${dim("│")} ${dim("You:")} ${fg(ACCENT)('"' + promptText + '"')} ${dim("│")} ${dim("You:")} ${fg(ACCENT)('"' + promptText + '"')}
${dim("│")} ${dim("Claude:")} ${fg(ACCENT)('"' + responseText + '"')}` ${dim("│")} ${dim("Claude:")} ${fg(ACCENT)('"' + responseText + '"')}`
} }
if (status === "idle") { if (status === "idle") {
return t` ${yellow("◉")} ${dim(prefix)} [${check}] ${dim(age.padEnd(9))} ${dim( return t` ${yellow("◉")} ${dim(prefix)} [${check}] ${dim(age.padEnd(9))} ${dim(
size.padEnd(7) size.padEnd(7)
)} ${fg(ACCENT)('"' + title + '"')} ${yellow("idle")} )} ${fg(ACCENT)('"' + title + '"')} ${yellow("idle")}${tabBadge}
${dim("│")} ${dim("You:")} ${fg(ACCENT)('"' + promptText + '"')} ${dim("│")} ${dim("You:")} ${fg(ACCENT)('"' + promptText + '"')}
${dim("│")} ${dim("Claude:")} ${fg(ACCENT)('"' + responseText + '"')}` ${dim("│")} ${dim("Claude:")} ${fg(ACCENT)('"' + responseText + '"')}`
} }

View File

@@ -125,21 +125,27 @@ export function updateTabBar() {
parts.push(t` ${dim("○ Picker")} `) parts.push(t` ${dim("○ Picker")} `)
} }
// Grid tabs // Grid tabs — inline pane names
for (const tab of app.gridTabs) { for (const tab of app.gridTabs) {
const count = app.directGrid?.getTabPaneCount(tab.id) ?? 0
const hasIdle = app.directGrid?.hasIdleInTab(tab.id) ?? false const hasIdle = app.directGrid?.hasIdleInTab(tab.id) ?? false
const isActive = app.viewMode === "grid" && app.directGrid?.activeTabId === tab.id const isActive = app.viewMode === "grid" && app.directGrid?.activeTabId === tab.id
const isPending = app.directGrid?.pendingCloseTabId === tab.id const isPending = app.directGrid?.pendingCloseTabId === tab.id
const label = `${tab.name} (${count})`
const closeBtn = isPending ? t` ${red(bold("[●]"))}` : t` ${dim("[×]")}` const closeBtn = isPending ? t` ${red(bold("[●]"))}` : t` ${dim("[×]")}`
// Build inline pane name list
const tabPanes = app.directGrid?.getTabPanes(tab.id) ?? []
const paneNames = tabPanes.map(p => {
const name = p.session.projectName
return name.length > 14 ? name.slice(0, 12) + "…" : name
})
const inlineLabel = paneNames.length > 0 ? paneNames.join(" · ") : "empty"
if (isActive) { if (isActive) {
parts.push(dim("╭"), t` ${cyan("●")} ${bold(label)}`, closeBtn, t` ${dim("╮")}`) parts.push(dim("╭"), t` ${cyan("●")} ${bold(inlineLabel)}`, closeBtn, t` ${dim("╮")}`)
} else if (hasIdle) { } else if (hasIdle) {
parts.push(t` ${yellow("◉")} ${label}`, closeBtn, " ", sep) parts.push(t` ${yellow("◉")} ${dim(inlineLabel)}`, closeBtn, " ", sep)
} else { } else {
parts.push(t` ${dim("○ " + label)}`, closeBtn, " ", sep) parts.push(t` ${dim("○ " + inlineLabel)}`, closeBtn, " ", sep)
} }
} }
@@ -151,11 +157,22 @@ export function updateTabBar() {
export function updateHeader() { export function updateHeader() {
const total = app.selectedProjects.size + app.selectedSessions.size const total = app.selectedProjects.size + app.selectedSessions.size
const modeLabel = app.demoMode ? " [DEMO]" : ""
// Add-pane mode: show target tab context
if (app.addPaneTargetTabId !== null) {
const targetTab = app.gridTabs.find(t => t.id === app.addPaneTargetTabId)
const tabName = targetTab?.name ?? `Tab ${app.addPaneTargetTabId}`
app.headerText.content = t` ${bold("cladm")}${yellow(modeLabel)}${cyan(bold(`Adding to: ${tabName}`))}${String(total)} selected ${dim(
`sort: ${app.sortLabels[app.sortMode]}${app.projects.length} projects`
)}`
return
}
// Count distinct tab groups // Count distinct tab groups
const tabGroups = new Set(app.selectedProjects.values()) const tabGroups = new Set(app.selectedProjects.values())
const tabNote = tabGroups.size > 1 ? `${tabGroups.size} tabs` : "" const tabNote = tabGroups.size > 1 ? `${tabGroups.size} tabs` : ""
const branchNote = app.selectedBranches.size > 0 ? ` (${app.selectedBranches.size} branch switch)` : "" const branchNote = app.selectedBranches.size > 0 ? ` (${app.selectedBranches.size} branch switch)` : ""
const modeLabel = app.demoMode ? " [DEMO]" : ""
const activeCount = app.projects.reduce((sum, p) => sum + (p.activeSessions > 0 ? 1 : 0), 0) const activeCount = app.projects.reduce((sum, p) => sum + (p.activeSessions > 0 ? 1 : 0), 0)
const busyCount = app.projects.reduce((sum, p) => sum + (p.busySessions > 0 ? 1 : 0), 0) const busyCount = app.projects.reduce((sum, p) => sum + (p.busySessions > 0 ? 1 : 0), 0)
const idleCount = activeCount - busyCount const idleCount = activeCount - busyCount
@@ -192,6 +209,12 @@ export function updateFooter() {
restoreHint = ` │ r restore (${paneCount}p, ${ago})` restoreHint = ` │ r restore (${paneCount}p, ${ago})`
} }
// Add-pane mode: simplified footer
if (app.addPaneTargetTabId !== null) {
app.footerText.content = t` ${dim("↑↓ nav │ space select │ → expand │ ← collapse │ enter add │ esc cancel")}`
return
}
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" + gridHint + restoreHint "↑↓ nav │ tab/shift-tab idle select │ enter focus │ i preview │ space select │ a all │ n none │ s sort │ q quit" + gridHint + restoreHint