- 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>
193 lines
7.0 KiB
TypeScript
193 lines
7.0 KiB
TypeScript
import {
|
|
t,
|
|
bold,
|
|
dim,
|
|
fg,
|
|
green,
|
|
yellow,
|
|
cyan,
|
|
magenta,
|
|
} from "@opentui/core"
|
|
import { app } from "../lib/state"
|
|
import { ACCENT } from "../lib/theme"
|
|
import { getSessionStatus } from "../data/monitor"
|
|
import { timeAgo, formatSize, elapsedCompact } from "../lib/time"
|
|
|
|
export function fmtSyncIndicator(ahead: number, behind: number): string {
|
|
if (ahead === -1 && behind === -1) return "✗"
|
|
if (ahead === 0 && behind === 0) return "✓"
|
|
const parts: string[] = []
|
|
if (ahead > 0) parts.push(`↑${ahead}`)
|
|
if (behind > 0) parts.push(`↓${behind}`)
|
|
return parts.join("")
|
|
}
|
|
|
|
export const TAB_COLORS = [
|
|
cyan, // 1
|
|
green, // 2
|
|
yellow, // 3
|
|
magenta, // 4
|
|
(s: string) => fg("#ff9e64")(s), // 5
|
|
(s: string) => fg("#7dcfff")(s), // 6
|
|
(s: string) => fg("#bb9af7")(s), // 7
|
|
(s: string) => fg("#73daca")(s), // 8
|
|
(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]!
|
|
return color(String(tabNum))
|
|
}
|
|
|
|
export function fmtProjectRow(project: import("../lib/types").Project, isSelected: number | undefined) {
|
|
let activeDot: string
|
|
let activeTag: string
|
|
if (project.activeSessions > 0) {
|
|
if (project.busySessions > 0) {
|
|
activeDot = green("●")
|
|
const count = String(project.activeSessions)
|
|
activeTag = project.activeSessions > 1 ? yellow((count + " ").slice(0, 2)) : " "
|
|
} else {
|
|
activeDot = yellow("◉")
|
|
const elapsed = elapsedCompact(project.lastActivityMs)
|
|
activeTag = elapsed ? dim((elapsed + " ").slice(0, 2)) : " "
|
|
}
|
|
} else {
|
|
activeDot = dim("○")
|
|
activeTag = " "
|
|
}
|
|
const check = fmtTabCheck(isSelected)
|
|
const arrow = project.expanded ? "▼" : "▶"
|
|
const name =
|
|
project.name.length > 28 ? project.name.slice(0, 25) + "..." : project.name
|
|
const branch =
|
|
project.branch.length > 8
|
|
? project.branch.slice(0, 7) + "…"
|
|
: project.branch
|
|
|
|
const sync = fmtSyncIndicator(project.ahead, project.behind)
|
|
const syncCol = sync === "✓" ? green(sync.padEnd(5))
|
|
: sync === "✗" ? dim(sync.padEnd(5))
|
|
: yellow(sync.padEnd(5))
|
|
|
|
const dirtyCol = project.dirty
|
|
? yellow(project.dirty.padEnd(9))
|
|
: green("clean".padEnd(9))
|
|
|
|
const ca = project.claudeAgo
|
|
let claudeCol
|
|
if (ca === "never" || ca === "-") claudeCol = dim(ca.padEnd(9))
|
|
else if (ca.includes("m ago") || ca.includes("h ago") || ca === "just now")
|
|
claudeCol = cyan(ca.padEnd(9))
|
|
else if (ca.includes("d ago")) claudeCol = green(ca.padEnd(9))
|
|
else claudeCol = dim(ca.padEnd(9))
|
|
|
|
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)
|
|
)} ${dim(String(project.totalMessages).padStart(5))} ${dim(project.tags)}`
|
|
}
|
|
|
|
export function fmtSessionRow(
|
|
projectIdx: number,
|
|
sessionIdx: number,
|
|
isSelected: boolean,
|
|
isLastSession: boolean
|
|
) {
|
|
const project = app.projects[projectIdx]
|
|
const session = project.sessions![sessionIdx]
|
|
const check = isSelected ? green("✓") : " " // sessions still use boolean check
|
|
const prefix = isLastSession ? "│ " : "├─"
|
|
const title =
|
|
session.title.length > 55
|
|
? session.title.slice(0, 52) + "..."
|
|
: session.title
|
|
const age = timeAgo(session.timestamp)
|
|
const size = formatSize(session.sizeBytes)
|
|
|
|
const status = getSessionStatus(project.path, session.id)
|
|
|
|
const promptText = session.lastUserPrompt
|
|
? session.lastUserPrompt.length > 60
|
|
? session.lastUserPrompt.slice(0, 57) + "..."
|
|
: session.lastUserPrompt
|
|
: "(no text)"
|
|
const responseText = session.lastAssistantMsg
|
|
? session.lastAssistantMsg.length > 60
|
|
? session.lastAssistantMsg.slice(0, 57) + "..."
|
|
: 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")}${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")}${tabBadge}
|
|
${dim("│")} ${dim("You:")} ${fg(ACCENT)('"' + promptText + '"')}
|
|
${dim("│")} ${dim("Claude:")} ${fg(ACCENT)('"' + responseText + '"')}`
|
|
}
|
|
return t` ${dim(prefix)} [${check}] ${dim(age.padEnd(9))} ${dim(
|
|
size.padEnd(7)
|
|
)} ${fg(ACCENT)('"' + title + '"')}
|
|
${dim("│")} ${dim("You:")} ${fg(ACCENT)('"' + promptText + '"')}
|
|
${dim("│")} ${dim("Claude:")} ${fg(ACCENT)('"' + responseText + '"')}`
|
|
}
|
|
|
|
export function fmtNewSessionRow(projectIdx: number, isSelected: number | undefined) {
|
|
const check = fmtTabCheck(isSelected)
|
|
return t` ${dim("└─")} [${check}] ${green("+ New session")}`
|
|
}
|
|
|
|
export function fmtBranchRow(projectIdx: number, branchName: string, isSelected: boolean) {
|
|
const project = app.projects[projectIdx]
|
|
const br = project.branches?.find(b => b.name === branchName)
|
|
if (!br) return t` ${dim("├─")} ${branchName}`
|
|
|
|
const check = isSelected ? green("✓") : " "
|
|
const sync = fmtSyncIndicator(br.ahead, br.behind)
|
|
const syncCol = sync === "✓" ? green(sync)
|
|
: sync === "✗" ? dim(sync)
|
|
: yellow(sync)
|
|
const msg = br.lastCommitMsg.length > 40 ? br.lastCommitMsg.slice(0, 37) + "..." : br.lastCommitMsg
|
|
|
|
return t` ${dim("├─")} [${check}] ${magenta(branchName.padEnd(18))} ${syncCol} ${dim(br.lastCommitAge.padEnd(9))} ${dim(msg)}`
|
|
}
|