5. 顶部 Header 与状态栏 StatusBar
这一篇讲主屏最上面和最下面两条 chrome:Header.tsx 和 StatusBar.tsx。两者都很短,但都体现了一个共同思路:按终端列宽分档降级,窄屏优先保住核心信息。
Header:logo 与版本
export function Header({ productVersion, columns }: HeaderProps) {
if (columns < HIDE_HEADER_BELOW_COLUMNS) {
return null;
}
if (columns < COMPACT_HEADER_BELOW_COLUMNS) {
return <Text color={geminiDarkTheme.colors.accentBlue}>KQode</Text>;
}
const versionLabel = ` v${productVersion}`;
return (
<Box>
<Text color={geminiDarkTheme.colors.accentBlue}>KQode</Text>
<Text color={geminiDarkTheme.colors.foreground}>{versionLabel}</Text>
</Box>
);
}
三档行为,和第 4 篇的 headerRowCount 一一对应:
- 列宽 < 36(
HIDE_HEADER_BELOW_COLUMNS):返回null,header 完全不渲染。极窄终端把这一行让给正文和输入框。 - 36 ≤ 列宽 < 52(
COMPACT_HEADER_BELOW_COLUMNS):只渲染蓝色KQode,省掉版本号。 - 列宽 ≥ 52:渲染
KQode+v<版本>,KQode用强调蓝accentBlue,版本号用前景白foreground,两段拼在一个Box里。
这里 productVersion 来自仓库根 Cargo.toml(第 1 篇 main.tsx),所以显示的是 KQode 产品版本,而不是 tui/ 包的版本。
Header 判断显隐用的 HIDE_HEADER_BELOW_COLUMNS / COMPACT_HEADER_BELOW_COLUMNS,正是 headerRowCount 用的同两个常量。两边共享常量,渲染出的行数才会和布局预算出的行数一致,否则就会出现“布局以为 header 占 1 行、实际渲染了 0 行”的错位。
StatusBar:提示与模型名
export function StatusBar({ columns, modelLabel }: StatusBarProps) {
const leftHints = columns >= 60 ? '/ commands | @ mention | ? help' : '/ | @ | ?';
const showModel = columns >= 60;
return (
<Box width={columns}>
<Text color={geminiDarkTheme.colors.muted}>{leftHints}</Text>
{showModel ? (
<Box flexGrow={1} justifyContent="flex-end">
<Text color={geminiDarkTheme.colors.accentGreen}>{modelLabel}</Text>
</Box>
) : null}
</Box>
);
}
状态栏分左右两块,以 60 列为界降级:
- 左侧提示
leftHints:宽屏显示完整的/ commands | @ mention | ? help,窄屏(< 60 列)压缩成/ | @ | ?。颜色用muted,因为这些只是提示。 - 右侧模型名:仅在 ≥ 60 列时显示。用
flexGrow={1}的Box加justifyContent="flex-end"把模型名顶到最右边,颜色用accentGreen。窄屏时整块不渲染,避免和左侧提示挤在一起。
一个重要约定:提示是“惰性”的
左侧的 /、@、? 看起来像可点击的命令入口,但在 U2 阶段它们只是文字。主屏计划文档明确要求这些 bottom hints 是 inert(惰性)的 muted 提示,不会触发任何命令、mention 或 help 行为。真正的斜杠命令、@ 引用、? 帮助都留到后续阶段。这也解释了为什么输入框里 /、@、? 只是普通字符(第 9 篇)——整套“命令系统”此刻还不存在,UI 先把位置占好。
为什么这两块这么简单
Header 和 StatusBar 是典型的“纯展示叶子组件”:只接收 props、只读主题色、不碰任何全局状态、不处理输入。它们的 props 由 HomeScreenView 里的 HomeHeader / HomeStatus 包装组件从 atom 里取出来喂进去:
function HomeHeader() {
const { columns, productVersion } = useAtomValue(homeScreenConfigAtom);
return <Header productVersion={productVersion} columns={columns} />;
}
function HomeStatus() {
const { columns, modelLabel } = useAtomValue(homeScreenConfigAtom);
return <StatusBar columns={columns} modelLabel={modelLabel} />;
}
这种“包装组件读 atom、叶子组件收 props”的分层,让叶子组件保持可单测、可复用,状态订阅集中在包装层。下一篇的 CwdLine 也是同样结构,但它背后多了 cwd 格式化和 Git 状态解析两块逻辑。