跳到主要内容

5. 顶部 Header 与状态栏 StatusBar

这一篇讲主屏最上面和最下面两条 chrome:Header.tsxStatusBar.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}BoxjustifyContent="flex-end" 把模型名顶到最右边,颜色用 accentGreen。窄屏时整块不渲染,避免和左侧提示挤在一起。

一个重要约定:提示是“惰性”的

左侧的 /@? 看起来像可点击的命令入口,但在 U2 阶段它们只是文字。主屏计划文档明确要求这些 bottom hints 是 inert(惰性)的 muted 提示,不会触发任何命令、mention 或 help 行为。真正的斜杠命令、@ 引用、? 帮助都留到后续阶段。这也解释了为什么输入框里 /@? 只是普通字符(第 9 篇)——整套“命令系统”此刻还不存在,UI 先把位置占好。

为什么这两块这么简单

HeaderStatusBar 是典型的“纯展示叶子组件”:只接收 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 状态解析两块逻辑。