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:
@@ -4,6 +4,67 @@ import { ensureGridView, createNewGridTab, switchToGridTab } from "../grid/view-
|
||||
import { loadSessions } from "../data/sessions"
|
||||
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() {
|
||||
if (app.selectedProjects.size === 0 && app.selectedSessions.size === 0) return
|
||||
if (app.demoMode) {
|
||||
|
||||
@@ -86,7 +86,7 @@ export class DirectGridRenderer {
|
||||
private tabBarHitRegions: { tabId: number, startCol: number, endCol: number }[] = []
|
||||
private tabCloseHitRegions: { tabId: number, startCol: number, endCol: number }[] = []
|
||||
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 }[] = []
|
||||
|
||||
// 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.
|
||||
// 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 {
|
||||
// Tab bar check (row 1)
|
||||
// Tab bar check (row 1) — includes inline pane names
|
||||
if (row === 1) {
|
||||
// Check close buttons first — widened ±1 around the × character
|
||||
for (const region of this.tabCloseHitRegions) {
|
||||
@@ -379,6 +379,12 @@ export class DirectGridRenderer {
|
||||
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) {
|
||||
if (col >= region.startCol && col <= region.endCol) {
|
||||
return { action: "tab", paneIndex: -1, tabId: region.tabId }
|
||||
@@ -390,15 +396,6 @@ export class DirectGridRenderer {
|
||||
}
|
||||
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)
|
||||
for (const i of indicesToCheck) {
|
||||
@@ -562,7 +559,7 @@ export class DirectGridRenderer {
|
||||
if (n === 0) return false
|
||||
const termW = process.stdout.columns || 120
|
||||
const termH = process.stdout.rows || 40
|
||||
const chromeTop = 4
|
||||
const chromeTop = 3
|
||||
const { cols } = this.calcGrid(n)
|
||||
const rows = Math.ceil(n / cols)
|
||||
const cellW = Math.floor(termW / cols)
|
||||
@@ -594,7 +591,7 @@ export class DirectGridRenderer {
|
||||
if (n === 0) return -1
|
||||
const termW = process.stdout.columns || 120
|
||||
const termH = process.stdout.rows || 40
|
||||
const chromeTop = 4
|
||||
const chromeTop = 3
|
||||
const { cols } = this.calcGrid(n)
|
||||
const rows = Math.ceil(n / cols)
|
||||
const cellW = Math.floor(termW / cols)
|
||||
@@ -614,13 +611,10 @@ export class DirectGridRenderer {
|
||||
|
||||
let out = SYNC_START
|
||||
|
||||
// Tab bar (row 1)
|
||||
// Tab bar (row 1) — includes inline pane names
|
||||
out += this.drawTabBar(termW)
|
||||
|
||||
// Pane list (row 2)
|
||||
out += this.drawPaneList(termW)
|
||||
|
||||
// Header (row 3)
|
||||
// Header (row 2)
|
||||
const n = this.panes.length
|
||||
const fi = this._focusIndex + 1
|
||||
let headerLeft: string, headerRight: string
|
||||
@@ -637,7 +631,7 @@ export class DirectGridRenderer {
|
||||
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}`
|
||||
}
|
||||
out += `\x1b[3;1H\x1b[${termW}X${headerLeft} ${headerRight}`
|
||||
out += `\x1b[2;1H\x1b[${termW}X${headerLeft} ${headerRight}`
|
||||
|
||||
// Pane borders + titles
|
||||
if (this.isExpanded) {
|
||||
@@ -671,6 +665,7 @@ export class DirectGridRenderer {
|
||||
private drawTabBar(termW: number): string {
|
||||
this.tabBarHitRegions = []
|
||||
this.tabCloseHitRegions = []
|
||||
this.paneListHitRegions = []
|
||||
this.tabBarAddBtnCol = -1
|
||||
|
||||
const RED_FG = hexFg("#f7768e")
|
||||
@@ -688,49 +683,106 @@ export class DirectGridRenderer {
|
||||
out += ` ${DIM}○ Picker${RESET} `
|
||||
}
|
||||
const pickerStart = pickerActive ? col + 1 : col + 1
|
||||
const pickerVisLen = pickerActive ? 10 : 10
|
||||
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) {
|
||||
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 tabPanes = this.tabPanes.get(tab.id) ?? []
|
||||
|
||||
const startCol = col + (isActive ? 2 : 1) // account for ╭ + space or just space
|
||||
const visLen = 2 + label.length // "● " + label
|
||||
// Build pane name list for this tab
|
||||
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 closeVisLen = 3
|
||||
|
||||
const tabStartCol = col
|
||||
|
||||
if (isActive) {
|
||||
// Chrome-style raised active tab
|
||||
out += `${TAB_BORDER}╭${RESET}${TAB_BG_ACTIVE} ${CYAN_FG}${BOLD}● ${label}${RESET}${TAB_BG_ACTIVE} ${closeText}${TAB_BG_ACTIVE} ${RESET}${TAB_BORDER}╮${RESET}`
|
||||
// ╭ + space + ● label + space + × + space + ╮
|
||||
const totalVis = 1 + 1 + visLen + 1 + closeVisLen + 1 + 1
|
||||
this.tabBarHitRegions.push({ tabId: tab.id, startCol, endCol: startCol + visLen - 1 })
|
||||
const closeStartCol = startCol + visLen + 1
|
||||
this.tabCloseHitRegions.push({ tabId: tab.id, startCol: closeStartCol, endCol: closeStartCol + closeVisLen - 1 })
|
||||
col += totalVis
|
||||
// Active tab: ╭ ● pane1 · ◉ pane2 × ╮
|
||||
out += `${TAB_BORDER}╭${RESET}${TAB_BG_ACTIVE} `
|
||||
col += 2 // ╭ + space
|
||||
|
||||
for (let pi = 0; pi < paneLabels.length; pi++) {
|
||||
const pl = paneLabels[pi]!
|
||||
let icon: string
|
||||
if (pl.status === "busy") icon = `${hexFg("#9ece6a")}●${RESET}`
|
||||
else if (pl.status === "idle") icon = `${hexFg("#e0af68")}◉${RESET}`
|
||||
else icon = `${DIM}○${RESET}`
|
||||
|
||||
const paneStartCol = col
|
||||
if (pl.isFocused) {
|
||||
out += `${TAB_BG_ACTIVE}${icon} ${hexFg(pl.color)}${BOLD}${pl.name}${RESET}`
|
||||
} else {
|
||||
// Inactive tab — flat, no border
|
||||
let indicator: string
|
||||
if (hasIdle) {
|
||||
indicator = `${YELLOW_FG}◉ ${label}${RESET}`
|
||||
} else {
|
||||
indicator = `${DIM}○ ${label}${RESET}`
|
||||
out += `${TAB_BG_ACTIVE}${icon} ${DIM}${pl.name}${RESET}`
|
||||
}
|
||||
out += ` ${indicator} ${closeText} ${DIM}│${RESET}`
|
||||
// space + ● label + space + × + space + │
|
||||
const totalVis = 1 + visLen + 1 + closeVisLen + 1 + 1
|
||||
this.tabBarHitRegions.push({ tabId: tab.id, startCol, endCol: startCol + visLen - 1 })
|
||||
const closeStartCol = startCol + visLen + 1
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if (paneLabels.length === 0) {
|
||||
out += `${TAB_BG_ACTIVE}${DIM}empty${RESET}`
|
||||
col += 5
|
||||
}
|
||||
|
||||
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 })
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
const pane = this.panes[index]!
|
||||
@@ -973,7 +982,7 @@ export class DirectGridRenderer {
|
||||
const n = count ?? this.panes.length
|
||||
const termW = process.stdout.columns || 120
|
||||
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 cellW = Math.floor(termW / cols)
|
||||
const cellH = Math.floor((termH - chromeTop - 1) / rows) // -1 for footer
|
||||
@@ -999,7 +1008,7 @@ export class DirectGridRenderer {
|
||||
repositionAll() {
|
||||
const termW = process.stdout.columns || 120
|
||||
const termH = process.stdout.rows || 40
|
||||
const chromeTop = 4
|
||||
const chromeTop = 3
|
||||
|
||||
if (this.isExpanded) {
|
||||
// Fullscreen: expanded pane gets all space
|
||||
|
||||
@@ -72,7 +72,7 @@ async function main() {
|
||||
try {
|
||||
const state = extractSessionState()
|
||||
if (state) saveSessionSync(state)
|
||||
} catch {}
|
||||
} catch (err) { console.error("[session-save]", err) }
|
||||
if (app.monitorInterval) { clearInterval(app.monitorInterval); app.monitorInterval = null }
|
||||
if (app.directGrid) app.directGrid.destroyAll()
|
||||
stopAllCaptures()
|
||||
@@ -187,7 +187,7 @@ async function main() {
|
||||
getUsageSummary().then(u => {
|
||||
app.cachedUsage = u
|
||||
updateUsagePanel()
|
||||
}).catch(() => {})
|
||||
}).catch(err => console.error("[usage]", err))
|
||||
|
||||
// Resize PTY panes when terminal window is resized
|
||||
process.stdout.on("resize", () => {
|
||||
@@ -220,7 +220,7 @@ async function main() {
|
||||
try {
|
||||
app.cachedUsage = await getUsageSummary()
|
||||
updateUsagePanel()
|
||||
} catch {}
|
||||
} catch (err) { console.error("[usage-poll]", err) }
|
||||
}
|
||||
|
||||
if (app.demoMode) {
|
||||
|
||||
@@ -225,19 +225,25 @@ function handlePickerTabBarClick(col: number, screenRow: number) {
|
||||
c = 11
|
||||
|
||||
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 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
|
||||
|
||||
if (isActive) {
|
||||
// Active: ╭ ● label × ╮ → c+1=╭, c+2=space, c+3..=● label, then close, ╮
|
||||
// Active: ╭ ● panes × ╮
|
||||
const labelStart = c + 2
|
||||
const labelEnd = labelStart + visLen - 1
|
||||
const closeCol = labelEnd + 2 // space + × position
|
||||
const totalVis = 1 + 1 + visLen + 1 + 1 + 1 + 1 // ╭ + sp + label + sp + × + sp + ╮
|
||||
const closeCol = labelEnd + 2
|
||||
const totalVis = 1 + 1 + visLen + 1 + 1 + 1 + 1
|
||||
|
||||
if (col === closeCol && dg) {
|
||||
const result = dg.requestCloseTab(tab.id)
|
||||
@@ -251,11 +257,11 @@ function handlePickerTabBarClick(col: number, screenRow: number) {
|
||||
}
|
||||
c += totalVis
|
||||
} else {
|
||||
// Inactive: sp ● label sp × sp │ → 1 + visLen + 1 + 1 + 1 + 1
|
||||
// Inactive: sp ● panes sp × sp │
|
||||
const labelStart = c + 1
|
||||
const labelEnd = labelStart + visLen - 1
|
||||
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) {
|
||||
const result = dg.requestCloseTab(tab.id)
|
||||
|
||||
@@ -49,6 +49,7 @@ export const app = {
|
||||
lastGridTabIndex: 0, // last active grid tab for Ctrl+Space toggle
|
||||
savedSession: null as SavedSession | null,
|
||||
restoreMode: null as "pending" | null,
|
||||
addPaneTargetTabId: null as number | null,
|
||||
|
||||
// UI refs (set during init)
|
||||
renderer: null as unknown as CliRenderer,
|
||||
|
||||
@@ -22,7 +22,7 @@ export function fmtSyncIndicator(ahead: number, behind: number): string {
|
||||
return parts.join("")
|
||||
}
|
||||
|
||||
const TAB_COLORS = [
|
||||
export const TAB_COLORS = [
|
||||
cyan, // 1
|
||||
green, // 2
|
||||
yellow, // 3
|
||||
@@ -34,6 +34,33 @@ const TAB_COLORS = [
|
||||
(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) {
|
||||
if (tabNum === undefined) return " "
|
||||
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 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.commitMsg || "-").padEnd(22)}${dirtyCol}${claudeCol}${dim(
|
||||
String(project.sessionCount).padStart(3)
|
||||
@@ -120,17 +148,19 @@ export function fmtSessionRow(
|
||||
: session.lastAssistantMsg
|
||||
: "(no text response)"
|
||||
|
||||
const tabBadge = session.id ? getSessionGridTabBadge(project.path, session.id) : ""
|
||||
|
||||
if (status === "busy") {
|
||||
return t` ${green("●")} ${dim(prefix)} [${check}] ${dim(age.padEnd(9))} ${dim(
|
||||
size.padEnd(7)
|
||||
)} ${fg(ACCENT)('"' + title + '"')} ${green("running")}
|
||||
)} ${fg(ACCENT)('"' + title + '"')} ${green("running")}${tabBadge}
|
||||
${dim("│")} ${dim("You:")} ${fg(ACCENT)('"' + promptText + '"')}
|
||||
${dim("│")} ${dim("Claude:")} ${fg(ACCENT)('"' + responseText + '"')}`
|
||||
}
|
||||
if (status === "idle") {
|
||||
return t` ${yellow("◉")} ${dim(prefix)} [${check}] ${dim(age.padEnd(9))} ${dim(
|
||||
size.padEnd(7)
|
||||
)} ${fg(ACCENT)('"' + title + '"')} ${yellow("idle")}
|
||||
)} ${fg(ACCENT)('"' + title + '"')} ${yellow("idle")}${tabBadge}
|
||||
${dim("│")} ${dim("You:")} ${fg(ACCENT)('"' + promptText + '"')}
|
||||
${dim("│")} ${dim("Claude:")} ${fg(ACCENT)('"' + responseText + '"')}`
|
||||
}
|
||||
|
||||
@@ -125,21 +125,27 @@ export function updateTabBar() {
|
||||
parts.push(t` ${dim("○ Picker")} `)
|
||||
}
|
||||
|
||||
// Grid tabs
|
||||
// Grid tabs — inline pane names
|
||||
for (const tab of app.gridTabs) {
|
||||
const count = app.directGrid?.getTabPaneCount(tab.id) ?? 0
|
||||
const hasIdle = app.directGrid?.hasIdleInTab(tab.id) ?? false
|
||||
const isActive = app.viewMode === "grid" && app.directGrid?.activeTabId === tab.id
|
||||
const isPending = app.directGrid?.pendingCloseTabId === tab.id
|
||||
const label = `${tab.name} (${count})`
|
||||
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) {
|
||||
parts.push(dim("╭"), t` ${cyan("●")} ${bold(label)}`, closeBtn, t` ${dim("╮")}`)
|
||||
parts.push(dim("╭"), t` ${cyan("●")} ${bold(inlineLabel)}`, closeBtn, t` ${dim("╮")}`)
|
||||
} else if (hasIdle) {
|
||||
parts.push(t` ${yellow("◉")} ${label}`, closeBtn, " ", sep)
|
||||
parts.push(t` ${yellow("◉")} ${dim(inlineLabel)}`, closeBtn, " ", sep)
|
||||
} 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() {
|
||||
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
|
||||
const tabGroups = new Set(app.selectedProjects.values())
|
||||
const tabNote = tabGroups.size > 1 ? ` → ${tabGroups.size} tabs` : ""
|
||||
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 busyCount = app.projects.reduce((sum, p) => sum + (p.busySessions > 0 ? 1 : 0), 0)
|
||||
const idleCount = activeCount - busyCount
|
||||
@@ -192,6 +209,12 @@ export function updateFooter() {
|
||||
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) {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user