diff --git a/src/actions/launch.ts b/src/actions/launch.ts index be05019..c3093f1 100644 --- a/src/actions/launch.ts +++ b/src/actions/launch.ts @@ -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) { diff --git a/src/components/direct-grid.ts b/src/components/direct-grid.ts index a5edae9..5968da7 100644 --- a/src/components/direct-grid.ts +++ b/src/components/direct-grid.ts @@ -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 - } else { - // Inactive tab — flat, no border - let indicator: string - if (hasIdle) { - indicator = `${YELLOW_FG}◉ ${label}${RESET}` - } else { - indicator = `${DIM}○ ${label}${RESET}` + // 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 { + 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 + │ - const totalVis = 1 + visLen + 1 + closeVisLen + 1 + 1 - this.tabBarHitRegions.push({ tabId: tab.id, startCol, endCol: startCol + visLen - 1 }) - const closeStartCol = startCol + visLen + 1 + + 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 diff --git a/src/index.ts b/src/index.ts index 1251798..82c2eae 100755 --- a/src/index.ts +++ b/src/index.ts @@ -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) { diff --git a/src/input/handlers.ts b/src/input/handlers.ts index 77cbc41..60489eb 100644 --- a/src/input/handlers.ts +++ b/src/input/handlers.ts @@ -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) diff --git a/src/lib/state.ts b/src/lib/state.ts index c13fba0..4305f5a 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -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, diff --git a/src/ui/formatters.ts b/src/ui/formatters.ts index 8acc010..630a8e4 100644 --- a/src/ui/formatters.ts +++ b/src/ui/formatters.ts @@ -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 + '"')}` } diff --git a/src/ui/panels.ts b/src/ui/panels.ts index 3d5692d..7285a45 100644 --- a/src/ui/panels.ts +++ b/src/ui/panels.ts @@ -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