侧边窗格与工作区视图
本页说明 TUI 工作区外壳如何根据 SidebarLayout 在主内容区旁边放置侧边窗格,或将侧边窗格以覆盖层形式呈现;同时覆盖同一外壳中用于交互提示的面板布局约束。当前源码证据集中在 AppShell 及其相关宽度计算逻辑,未发现足够源码证据来定义具体业务视图、窗格切换命令或持久化状态。
Purpose and Scope
本页的边界是 apps/zcode-cli/packages/tui/src/app-components.tsx 中的工作区级布局职责:
AppShell如何拆分主区域、停靠式侧边窗格和覆盖式侧边窗格;SidebarLayout.visible与SidebarLayout.overlay如何决定渲染分支;- 主区域的最小宽度、内边距,以及侧边窗格存在时的间距;
- 面板内容宽度如何从终端宽度和侧边窗格宽度计算出来。
具体的 SidebarLayout 类型定义、侧边栏内容组件、视图注册机制、键盘快捷键和工作区状态来源在本次受限源码读取中未能读取;因此不对这些行为作推断。若需要了解具体视图的业务内容,应参阅对应的 TUI 视图或侧边栏页面。
Overview
AppShell 是一个 React 组件工厂式的 TUI 布局外壳。它接收三个关键输入:主内容 children、可选的 onMouseUp 回调,以及由调用方提供的 sidebar 节点和 sidebarLayout 布局状态。
实现先把布局状态归一化为两个局部结果:当侧边栏可见且不是覆盖模式时,节点进入主容器右侧;当侧边栏可见且是覆盖模式时,节点进入一个绝对定位的覆盖层。两个结果互斥,因此同一个侧边栏节点不会同时以停靠和覆盖两种方式渲染。
主内容区始终保留 APP_MAIN_MIN_WIDTH 的最小宽度(源码值为 40),并使用列方向布局。停靠式侧边栏存在时,主内容区额外设置右边距;覆盖式侧边栏则不改变主内容的布局尺寸,而是通过绝对定位和较高的 zIndex 叠加在工作区上方。这种区分使覆盖模式能够在不重新计算主内容宽度的情况下展示侧边窗格。
Architecture
图中关系均来自 AppShell 的实际实现:组件读取 sidebarLayout.visible 和 sidebarLayout.overlay,将 sidebar 节点分别赋给 dockedSidebar 或 overlaySidebar,然后将主内容、停靠内容和覆盖内容组合到外层 box 中。SidebarLayout 的字段定义本身未在本次源码窗口中读取,因此图中只表示 AppShell 对字段的使用,不扩展其余状态语义。
Source: app-components.tsx
布局分支与渲染结构
AppShell 的核心决策发生在渲染之前:
sidebarLayout.visible && !sidebarLayout.overlay成立时,sidebar被赋给dockedSidebar;sidebarLayout.visible && sidebarLayout.overlay成立时,sidebar被赋给overlaySidebar;- 外层
box使用行方向布局,让主内容和停靠窗格可以横向排列; - 主内容内部使用列方向布局,并承载调用方传入的
children; - 覆盖窗格存在时,再创建一个覆盖整个外层区域的绝对定位
box,并将侧边栏对齐到右侧。
这里的 sidebar 是已经构造好的 React 节点,而不是由 AppShell 创建的具体视图。因而 AppShell 负责布局,不负责决定侧边窗格显示什么内容。
停靠式布局
停靠式侧边栏作为外层行布局中的兄弟节点渲染。主内容在该模式下设置 marginRight: 1,避免主区域与侧边栏直接贴合;同时主区域仍保持 minWidth: APP_MAIN_MIN_WIDTH。
覆盖式布局
覆盖式侧边栏不作为正常流布局的兄弟节点参与尺寸计算,而是放入 position: "absolute" 的容器。该容器具有 top、right、bottom、left 全覆盖定位,并设置 zIndex: 10;其 alignItems: "flex-end" 将窗格推向右侧。覆盖背景来自 SIDEBAR_OVERLAY_BACKGROUND,说明覆盖模式有独立的视觉底层,但源码片段没有给出该常量的具体颜色值。
核心布局流程
该流程只描述源码明确实现的布局选择,不代表未读取的输入事件或状态管理流程。onMouseUp 会被直接挂到最外层 box,但其调用方和具体行为不在本页源码证据范围内。
Source: app-components.tsx
宽度计算与面板约束
同一文件还公开了 actionPanelContentWidthForTerminal,用于从终端总宽度推导动作面板可用内容宽度。计算顺序是:
- 当
sidebarWidth > 0时,将主区域右边距计为 1,否则为 0; - 从终端宽度中扣除侧边栏宽度、应用外壳水平内边距(2 列)和上述右边距;
- 通过
Math.max(APP_MAIN_MIN_WIDTH, ...)保证主区域计算宽度至少为 40; - 再扣除面板自身的水平装饰宽度(4 列);
- 将结果交给
normalizeSlashCommandContentWidth做最终归一化。
因此,侧边栏越宽,动作面板的内容空间通常越小;但主区域不会低于源码定义的最小值。最终结果还受 normalizeSlashCommandContentWidth 的规则约束,本次读取的片段未包含该函数实现,故不具体列出其下限或舍入行为。
1export function actionPanelContentWidthForTerminal(
2 terminalWidth: number,
3 sidebarWidth: number,
4): number {
5 const mainMarginRight = sidebarWidth > 0 ? APP_MAIN_MARGIN_RIGHT_COLUMNS : 0;
6 const mainWidth = Math.max(
7 APP_MAIN_MIN_WIDTH,
8 Math.floor(terminalWidth) -
9 sidebarWidth -
10 APP_SHELL_HORIZONTAL_PADDING_COLUMNS -
11 mainMarginRight,
12 );
13 return normalizeSlashCommandContentWidth(mainWidth - PANEL_HORIZONTAL_CHROME_COLUMNS);
14}Source: app-components.tsx
这段实现的设计重点是先保护工作区主区域,再计算面板内容宽度,而不是直接把终端宽度按比例分配。这样可以避免在窄终端或侧边栏较宽时把主区域压缩到不可用状态;不过当主区域触及最小宽度后,面板的实际可用空间仍取决于归一化函数和终端渲染器的行为。
相关面板:SlashSuggestionPanel
app-components.tsx 中的 SlashSuggestionPanel 也遵循明确的尺寸约束。它在 commands.length === 0 时直接返回 null,否则调用 visibleSlashCommandWindow,最多展示源码常量 SLASH_COMMAND_VISIBLE_COUNT 指定的 6 条命令。每一行根据命令摘要计算高度,再将行高累加并加上两行面板装饰高度。
这说明面板并非固定使用终端高度:可见命令窗口和摘要换行共同决定面板高度。面板本身使用 palette.panel 背景、边框和列方向布局;选中行将选择标记和命令文字设为强调色,摘要则保持可换行且允许收缩。
1export function SlashSuggestionPanel({
2 commands,
3 contentWidth,
4 copy = DEFAULT_TUI_COPY,
5 selectedIndex,
6}: {
7 commands: readonly SlashCommand[];
8 contentWidth?: number;
9 copy?: TuiCopy;
10 selectedIndex: number;
11}): React.ReactElement | null {
12 if (commands.length === 0) return null;
13 const visible = visibleSlashCommandWindow(commands, selectedIndex, SLASH_COMMAND_VISIBLE_COUNT);
14 const rowContentWidth = normalizeSlashCommandContentWidth(contentWidth);
15 // Fixed one-row heights let wrapped text draw over later commands in narrow terminals.
16 const rows = visible.commands.map((command, index) => {
17 const selected = index === visible.selectedIndex;
18 const parts = slashCommandRowParts(command, selected);
19 return {
20 command,
21 parts,
22 selected,
23 height: slashCommandRowHeight(parts, rowContentWidth),
24 };
25 });
26 const panelHeight =
27 SLASH_COMMAND_PANEL_CHROME_ROWS + rows.reduce((height, row) => height + row.height, 0);Source: app-components.tsx
注释明确指出窄终端下换行文本不能侵入后续命令行,因此每行高度必须在渲染前计算。这是该组件在工作区视图中保持可读性的关键约束。
Usage Examples
组合工作区外壳
以下是源码中 AppShell 的实际组件签名和布局分支。调用方需要提供 sidebar 与 sidebarLayout;本页没有足够证据展示某个具体调用点,因此不虚构调用代码。
1export function AppShell({
2 children,
3 onMouseUp,
4 sidebar,
5 sidebarLayout,
6}: {
7 children: React.ReactNode;
8 onMouseUp?: () => void;
9 sidebar: React.ReactNode | null;
10 sidebarLayout: SidebarLayout;
11}): React.ReactElement {
12 const dockedSidebar = sidebarLayout.visible && !sidebarLayout.overlay ? sidebar : null;
13 const overlaySidebar = sidebarLayout.visible && sidebarLayout.overlay ? sidebar : null;
14
15 return h(
16 "box",
17 {
18 onMouseUp,
19 style: {
20 backgroundColor: palette.background,
21 flexDirection: "row",
22 height: "100%",
23 padding: 0,
24 width: "100%",
25 },
26 },
27 h(
28 "box",
29 {
30 style: {
31 flexDirection: "column",
32 flexGrow: 1,
33 height: "100%",
34 marginRight: dockedSidebar ? 1 : 0,
35 minWidth: APP_MAIN_MIN_WIDTH,
36 padding: 1,
37 },
38 },
39 children,
40 ),
41 dockedSidebar,
42 overlaySidebar
43 ? h(
44 "box",
45 {
46 style: {
47 alignItems: "flex-end",
48 backgroundColor: SIDEBAR_OVERLAY_BACKGROUND,
49 bottom: 0,
50 left: 0,
51 position: "absolute",
52 right: 0,
53 top: 0,
54 zIndex: 10,
55 },
56 },
57 overlaySidebar,
58 )
59 : null,
60 );
61}Source: app-components.tsx
Configuration Options
本次读取的实现没有发现环境变量、配置文件或用户可调参数。可见的布局值是模块常量,而不是外部配置项:
| 常量 | 类型 | 源码值 | 用途 |
|---|---|---|---|
APP_MAIN_MIN_WIDTH | number | 40 | 主区域的最小宽度 |
APP_SHELL_HORIZONTAL_PADDING_COLUMNS | number | 2 | 工作区水平内边距计算 |
APP_MAIN_MARGIN_RIGHT_COLUMNS | number | 1 | 有侧边栏时的主区域右边距 |
PANEL_HORIZONTAL_CHROME_COLUMNS | number | 4 | 从面板总宽度中扣除的水平装饰宽度 |
SLASH_COMMAND_VISIBLE_COUNT | number | 6 | Slash 建议面板的最大可见命令数 |
SLASH_COMMAND_PANEL_CHROME_ROWS | number | 2 | Slash 建议面板的装饰行数 |
这些值在当前实现中以模块级 const 固定,源码没有显示运行时覆盖机制。
API Reference
AppShell(props): React.ReactElement
渲染工作区的主容器、停靠侧边窗格或覆盖侧边窗格。
参数:
children(React.ReactNode):主工作区内容。onMouseUp(() => void,可选):绑定到最外层box的鼠标释放回调。sidebar(React.ReactNode | null):由调用方提供的侧边窗格节点。sidebarLayout(SidebarLayout):至少需要被本实现读取的字段是visible和overlay。
返回值: React.ReactElement,由 TUI box 元素组成的工作区布局。
异常: 源码片段没有声明或主动抛出异常。
actionPanelContentWidthForTerminal(terminalWidth: number, sidebarWidth: number): number
根据终端宽度和侧边栏宽度计算动作面板的内容宽度。
参数:
terminalWidth(number):当前终端宽度;实现先对其调用Math.floor。sidebarWidth(number):侧边栏宽度;大于 0 时会增加主区域右边距扣除项。
返回值: number,经过 normalizeSlashCommandContentWidth 归一化后的内容宽度。
异常: 源码片段没有声明或主动抛出异常。
SlashSuggestionPanel(props): React.ReactElement | null
渲染 Slash 命令建议面板;命令列表为空时返回 null。
参数: commands、可选的 contentWidth、可选的 copy 和必需的 selectedIndex。实现使用 visibleSlashCommandWindow 截取可见窗口,并根据每行内容计算面板高度。
返回值: 命令为空时为 null,否则为建议面板 React 元素。
异常: 源码片段没有声明或主动抛出异常。
Failure Modes、边界条件与并发
空侧边栏或不可见状态
sidebar 可以是 null。当 visible 为假时,即使传入了非空节点,两个局部变量也都会是 null,因此 AppShell 只保留主内容。源码没有显示不可见状态下对侧边栏节点进行卸载之外的清理逻辑。
覆盖层与主区域重叠
覆盖模式使用绝对定位并覆盖四个方向,且 zIndex 为 10。该实现有意让覆盖窗格浮在主区域上方;但源码没有定义点击穿透、焦点管理、Esc 关闭或遮罩交互,因此这些行为不能从本页证据中确认。
窄终端
主区域通过 Math.max 保留最小宽度;Slash 建议面板则通过逐行高度和摘要换行处理窄终端。源码注释特别指出,如果使用固定的一行高度,换行文本可能绘制到后续命令上,因此当前实现选择先计算行高。未读取到渲染器在终端实际小于最小宽度时的截断策略。
并发与共享状态
当前读取的组件均为同步纯渲染函数,没有异步操作、锁、共享可变缓存或并发控制。sidebarLayout 和 selectedIndex 作为渲染输入传入;状态如何更新属于调用方职责,源码证据不足以描述其一致性策略。
Performance and Operational Notes
AppShell的布局分支只进行布尔判断和 React 元素组合,未见网络、文件系统或数据库操作。actionPanelContentWidthForTerminal为常数级算术计算。SlashSuggestionPanel遍历当前可见命令窗口,而不是无界遍历全部命令;源码常量将窗口限制为 6 条。- 每次渲染都会根据可见命令计算行高和面板总高;这保证换行布局准确,但本片段未提供基准测试或缓存机制。
- 侧边栏宽度会直接参与面板内容宽度计算,因此扩展侧边栏时应同时验证窄终端下的主区域和建议面板表现。
Extension Points
从已读源码可以确认的扩展点只有组件输入:调用方可以替换 children、sidebar、sidebarLayout 和可选的 onMouseUp,而不需要修改 AppShell 的布局代码。具体实现新的侧边栏视图时,应保证其节点能够在正常流布局和绝对定位覆盖容器中工作;但侧边栏自身所需接口、尺寸协议和生命周期在本次读取中未找到。
SlashSuggestionPanel 允许调用方通过 copy 提供本地化文本,通过 contentWidth 影响换行宽度;命令集合和选中索引也由调用方控制。这些输入是源码明确支持的定制方式。
Tests
在受限的 6 次源码探索预算内没有读取测试文件,因此无法从源代码确认 AppShell、侧边窗格分支或宽度函数的测试覆盖情况。实现层面建议至少验证以下已知分支,但这里不将其表述为现有测试:不可见、停靠可见、覆盖可见、sidebar 为 null、终端宽度小于最小值,以及 Slash 命令为空或摘要换行。
Related Links
本页未列出未读取的源文件作为确定性引用;SidebarLayout 的定义、具体侧边栏内容和视图注册流程需要在后续源码范围允许时单独补充。