Warp 垂直标签栏 View as Panes / Tabs:从面板粒度到标签粒度的总览模式实现解析

原创2026-10-03 20:23:411,621 阅读
文章标签:人工智能AI Agent代码智能体桌面应用

Warp 垂直标签栏 View as Panes / Tabs:从面板粒度到标签粒度的总览模式实现解析

导读

本文围绕 Warp 的 specs/APP-3828 产品与技术规格,深入讲解垂直标签栏(Vertical Tabs)新增的 View as 切换功能:在 Panes(每个面板一行)与 Tabs(每个标签一行,以活动面板为代表)两种行粒度之间即时切换,并作为同步设置跨会话持久化。读完本文,你将掌握该功能的完整交互设计、PaneGroup::focused_pane_id 作为代表行数据源的底层原理,以及 pane_ids_for_display_granularity 如何以最小风险统一渲染与搜索两条代码路径的实现方案。

背景:面板中心视图的问题

Warp 的垂直标签栏原本是**面板中心(pane-centric)**的:每个可见面板(pane)在所属标签(tab)下渲染为独立的一行。当用户需要细粒度地看清每个 split 时,这种设计是合理的;但当标签内包含多个面板、而用户只想在标签层面快速扫描整个工作区时,面板行会形成噪音——每个标签组头下方排满了行,难以一眼判断"这个标签里当前正在做什么"。

APP-3828 的目标是在不引入全新视觉语言的前提下,提供一种更高层级的总览模式:把每个标签压缩为一行代表行(representative row)。它保留现有标签结构、复用现有面板行的 UI,因此第一版既易于理解又风险可控。

核心功能:View as 设置

垂直标签栏的显示选项弹出面板(display options popup)新增一个顶层设置,以两段式切换控件(two-segment toggle)呈现在弹窗顶部、现有显示控件之上:

  • View as: Panes —— 默认值,即当前行为
  • View as: Tabs —— 新总览模式

点击 Panes 或 Tabs 后面板立即更新,且弹窗保持打开,让用户能在上下文中实时看到变化。切换模式不会重置当前密度(Density)或任何已有面板行显示偏好。

┌─ Vertical Tabs Options ────────────┐
│  View as        [ Panes | Tabs ]   │
│  ───────────────────────────────   │
│  Density        [ ▭ | ▭▭ ]        │
│  Pane title as  ▾ ...              │
│  Additional metadata  ▾ ...       │
│  Show           ▾ ...              │
└────────────────────────────────────┘

Panes 模式:保持现状

当 View as = Panes 时,面板行为与今天完全一致:

  • 每个可见面板渲染为自己的条目;
  • 条目按标签组头分组;
  • compact / expanded 密度行为不变;
  • 现有面板行显示偏好继续生效。

除弹窗中新增的 View as 控件外,Panes 模式不应引入任何视觉或行为变化——这是保证老用户零感知迁移的默认路径。

Tabs 模式:每标签一行

当 View as = Tabs 时,面板仍保持标签分组结构,但每个标签组头下方只渲染一行代表行,而不是每个可见面板一行。

代表行的数据源永远是该标签的活动面板(active pane)。因此在本迭代中,View as = Tabs 实际上意味着标签条目用其**聚焦会话(Focused session)**来命名和渲染——即 Focused session 是 Tabs 模式下唯一支持的行为,且是隐式生效的,不提供额外命名开关。

本迭代有意不实现探索性原型(exploratory mock)中出现的 Tabs-only Default name 区块(含 Focused session 与 Summary 两个选项),也不引入 Summary 命名模式或其他替代标签命名策略;同时不会把面板扁平化为无表头的标签列表,也不会重新设计标签组头、关闭按钮、重命名或拖拽行为。

代表行的外观与更新规则

外观完全复用现有面板行 UI

代表行复用活动面板在 Panes 模式下本应使用的面板行渲染器:

  • compact 密度:使用活动面板在 Panes 模式下使用的同一紧凑行渲染器;
  • expanded 密度:使用同一展开行渲染器。

这包括相同的图标规则、标题规则、副标题/元数据规则、徽标(badges)、截断行为与选中样式。正因为代表行本质仍是一行"面板样式行",现有面板行显示控件在 Tabs 模式下依然生效且不会被隐藏:

  • Pane title as 仍会改变终端代表行的标注方式;
  • Additional metadata 仍影响紧凑终端代表行;
  • Show 仍影响展开终端代表行。

代表行实时更新

每当该标签的活动面板发生变化,代表行都会立即更新,典型场景包括:

  • 用户在标签内多个 split 面板间切换焦点;
  • 活动面板被关闭,另一个面板成为活动面板;
  • 活动面板显示的元数据变化(如终端标题、分支、徽标或未保存状态)。

Tabs 模式始终反映标签当前的活动面板,而不是用户刚切到 Tabs 模式时恰好处于活动状态的面板。

单面板标签与多面板标签的差异

  • 单面板标签:标签内只有一个可见面板时,Tabs 模式与 Panes 模式在组头下方的显示效果基本一致(因为活动面板即唯一面板)。
  • 多面板标签:Panes 模式为每个可见面板渲染一行;Tabs 模式仅为该标签渲染一行(基于活动面板)。非活动面板在 Tabs 模式下不会获得独立行。

Tabs 模式的交互与搜索行为

行交互

代表行沿用其复用的活动面板行的交互语义:

  • 点击行:激活该标签并聚焦其活动面板;
  • 选中/高亮状态:继续表示活动标签/聚焦面板。

本迭代不引入任何 Tabs 模式独有的新行级交互。

搜索/过滤

搜索过滤作用于当前模式下实际渲染的条目:

  • Panes 模式下匹配保持"基于面板"的现状;
  • Tabs 模式下匹配只基于每个标签的代表行。

在第一迭代中,被 Tabs 模式隐藏的非活动面板不会产生独立的匹配结果——隐藏面板不参与搜索。

标签组头完全不受影响

Tabs 模式不会移除或重新设计现有标签组头,以下行为保持原样:

  • 标签标题显示;
  • 面板数量显示;
  • 重命名行为;
  • 关闭行为;
  • 拖拽行为;
  • 现有组头右键菜单行为。

Tabs 模式唯一改变的是组头下方渲染的行数:每个标签一行,而非每面板一行。

源码实现:新同步设置枚举

产品行为落地的第一步,是在 app/src/workspace/tab_settings.rs 中新增行粒度枚举 VerticalTabsDisplayGranularity(当前位于该文件 L344-L359),并在 TabSettings 中用 implement_setting_for_enum! 宏注册,继承既有垂直标签栏设置的同步/层级行为:

#[derive(
    Default,
    Debug,
    serde::Serialize,
    serde::Deserialize,
    PartialEq,
    Copy,
    Clone,
    schemars::JsonSchema,
    settings_value::SettingsValue,
)]
#[schemars(
    description = "Granularity of rows displayed in the vertical tabs panel.",
    rename_all = "snake_case"
)]
pub enum VerticalTabsDisplayGranularity {
    #[default]
    Panes,
    Tabs,
}

settings::macros::implement_setting_for_enum!(
    VerticalTabsDisplayGranularity,
    TabSettings,
    SupportedPlatforms::ALL,
    SyncToCloud::Globally(RespectUserSyncSetting::Yes),
    surface: settings::SettingSurfaces::GUI,
    private: false,
    toml_path: "appearance.vertical_tabs.display_granularity",
    description: "Granularity of rows displayed in the vertical tabs panel.",
);

关键点:

  • #[default] Panes 保证了老用户默认行为零变化;
  • SupportedPlatforms::ALL 与 SyncToCloud::Globally(RespectUserSyncSetting::Yes) 使它成为跨会话、跨设备的同步设置(对应 PRODUCT.md 成功标准第 13 条);
  • TOML 配置路径为 appearance.vertical_tabs.display_granularity,与既有的 view_mode、primary_info、compact_subtitle 同层级(见同一文件中 VerticalTabsViewMode 等既有枚举的注册方式)。

值得注意:该枚举的 TOML 层级 appearance.vertical_tabs.* 与 implement_setting_for_enum! 中 hierarchy 归属一致,且刻意不复用/不重命名既有的 VerticalTabsViewMode(它在代码里表示 compact/expanded 密度)。这是产品文案"View as"与技术枚举名之间的一次刻意解耦:UI 上把原控件改标为 Density,底层枚举名暂不迁移,避免无关逻辑的连锁改动。

源码实现:WorkspaceAction 与状态持久化

Action 定义

在 app/src/workspace/action.rs(L390 附近)新增与既有垂直标签栏设置并列的 action:

SetVerticalTabsDisplayGranularity(VerticalTabsDisplayGranularity),

状态写入与持久化豁免

Workspace::handle_action 中对该 action 的处理与其它设置写入完全一致:读取枚举载荷 → 调用 settings.vertical_tabs_display_granularity.set_value(...) → ctx.notify() 触发重绘(见 app/src/workspace/view.rs 中 SetVerticalTabsViewMode 等既有处理器的写法)。

关键细节在于 should_save_app_state_on_action:该 action 与 SetVerticalTabsViewMode、SetVerticalTabsPrimaryInfo、SetVerticalTabsCompactSubtitle 一样,返回 false(见 action.rs 附近),因为持久化由设置框架负责,而非工作区快照。对应的回归测试位于 app/src/workspace/action_tests.rs(现有测试已覆盖 SetVerticalTabsViewMode 等"不保存工作区状态"的断言模式)。

弹窗鼠标状态与分段控件

VerticalTabsPanelState 为新的两段式控件新增两个 MouseStateHandle(Panes 段与 Tabs 段),既有 compact_segment_mouse_state / expanded_segment_mouse_state 保持不变,密度控件无需重构。弹窗点击 Tabs 段时派发 WorkspaceAction::SetVerticalTabsDisplayGranularity(granularity)(见 app/src/workspace/view/vertical_tabs.rs 附近),弹窗保持打开、原地更新。

源码实现:行粒度选择器(核心结构变更)

TECH.md 反复强调:本功能"最重要的结构性变更"是把"该标签当前应渲染/搜索哪些面板 id"的决定集中到一处。落地即为 app/src/workspace/view/vertical_tabs.rs 中的纯函数:

fn pane_ids_for_display_granularity(
    visible_pane_ids: &[PaneId],
    focused_pane_id: PaneId,
    granularity: VerticalTabsDisplayGranularity,
) -> Vec<PaneId> {
    match granularity {
        VerticalTabsDisplayGranularity::Panes => visible_pane_ids.to_vec(),
        VerticalTabsDisplayGranularity::Tabs => visible_pane_ids
            .iter()
            .copied()
            .find(|pane_id| *pane_id == focused_pane_id)
            .or_else(|| visible_pane_ids.first().copied())
            .into_iter()
            .collect(),
    }
}

行为矩阵:

输入条件 Panes 模式返回值 Tabs 模式返回值
可见面板列表非空、聚焦面板在列表中 全部可见面板(保持原有顺序) 仅聚焦面板一个 id
可见面板非空、聚焦面板不在列表(关闭/恢复边界) 全部可见面板 首个可见面板(防御性回退)
标签无可见面板 空 vec 空 vec

该函数刻意保持纯函数、小体积、可单测,并在 vertical_tabs_tests.rs 中覆盖上述四种情况。

渲染与搜索:同一选择器的三处接入点

为避免"渲染一行、搜索全部"的漂移,pane_ids_for_display_granularity 被统一用在三条代码路径中(均位于 app/src/workspace/view/vertical_tabs.rs):

  1. matching_tab_indices(L1177 附近):决定哪些标签参与键盘导航与搜索结果记账。原来遍历所有可见面板,现在按粒度选择面板集合;
  2. render_groups(L1766 附近)中的搜索分支:计算 matching_ids 决定搜索时渲染哪些行;
  3. render_tab_group(L2061 附近)的行构建路径:决定组头下方渲染哪些行。

三处都读取同一份新设置,并替换对 visible_pane_ids() 的直接迭代。这保证了 Tabs 模式语义处处一致:

  • 标签只渲染其代表行;
  • 搜索只考虑该代表行;
  • 被搜索隐藏的标签由同一代表行判定。

行渲染器与 PaneProps 保持原样

关键设计决策:本迭代不引入新的"标签行"prop 类型。代表行仍然从选中的 PaneId 构建普通 PaneProps,并复用 render_pane_row(expanded 密度)、render_compact_pane_row(compact 密度)、render_pane_row_element。这保留了:

  • 行点击行为(FocusPane);
  • 既有元数据与徽标规则;
  • compact/expanded 密度行为;
  • 选中与悬停样式;
  • 与本分支上已在推进的面板行改进的兼容性。

render_tab_group 同时负责组容器、可选自定义标题头、悬停背景与悬浮操作条(action belt),本迭代不 fork 该结构——只有行列表随粒度变化,其余保持不变,从而把重命名、标签操作与拖拽的回归风险降到最低。

为什么代表行数据源是 focused_pane_id 而非 active_session_id

TECH.md 明确排除了 active_session_id() 作为代表行来源,原因有二:

  1. active_session_id() 仅适用于终端面板,在 code / notebook / workflow 标签下会失效;
  2. focused_pane_id() 对任意面板类型都有效,且它正是标签重新激活时会被聚焦的面板——语义与"Focused session"完全吻合。

仓库证据:PaneGroup::focused_pane_id(app/src/pane_group/mod.rs)直接读取 focus_state 的 focused_pane_id();而 PaneGroup::display_title()(同文件 L5234 附近)早已把聚焦面板作为标签级显示状态的来源。因此当用户在 split 标签内切换焦点时,PaneGroup::focus_pane 更新 focus_state,下一次渲染自动产生不同的代表行——实时更新无需额外通知机制。

风险与缓解设计

风险 缓解方案
既有 VerticalTabsViewMode(密度)与产品新文案 View as(粒度)语义混淆 新增独立命名枚举 VerticalTabsDisplayGranularity,不重载 VerticalTabsViewMode;仅 UI 层面重标为 Density
关闭/恢复等边界下聚焦面板暂不在可见列表中 选择器回退到首个可见面板;仅当标签真正无可见面板时才返回空 vec
渲染与搜索路径漂移(渲染一行却搜全部) 三处调用路径共用同一个粒度选择器,面板 id 选择逻辑只保留一份
探索原型诱使弹窗逻辑超出范围 本票仅交付顶层 View as 控件;不添加 Tabs-only Default name/Summary 区块;Tabs 模式下也不隐藏既有面板行控件

测试与验证清单

单元测试

手工验证

  • 打开显示选项弹窗,确认 View as 位于既有控件之上,切换时面板立即更新且弹窗保持打开;
  • 默认设置下逐可见面板渲染一行,行为与之前完全一致;
  • 单面板标签在两种模式间切换,组头下方无实质变化;
  • 多面板标签:Panes 显示全部面板行,Tabs 恰好一行;
  • 多面板标签内切换面板焦点,Tabs 代表行随之更新;
  • compact 与 expanded 两种密度下代表行均正常;
  • Tabs 模式下修改 Pane title as、Additional metadata、Show,确认它们仍作用于代表行;
  • Tabs 模式下组头重命名、关闭、面板计数、拖拽与右键菜单正常;
  • 选择 Tabs 后重启 Warp,面板以 Tabs 模式重新打开(同步设置持久化);
  • 搜索:多面板标签中仅活动面板的代表行被匹配渲染。

后续工作

  • 若希望代码术语与产品 UI 完全一致,可将 VerticalTabsViewMode 重命名为密度相关名称(非本票必要);
  • 待产品行为最终确定后,在独立工单中实现 Tabs-only 命名控件(Focused session vs Summary);届时 Pane title as 等面板行控件可改为仅针对聚焦会话路径有条件显示,而非在 Tabs 下始终展示。

总结

APP-3828 以"最小风险交付新语义"为设计主线:用一个新的同步设置 VerticalTabsDisplayGranularity 表达行粒度,用 PaneGroup::focused_pane_id() 作为标签级代表行的权威数据源,用纯函数 pane_ids_for_display_granularity 统一渲染与搜索的选型逻辑,并完全复用既有面板行渲染器。对开发者而言,这套"新增设置枚举 + 新增 action + 集中选择器 + 复用渲染器"的模式,是给大型工作区 UI 添加总览模式时值得参考的低风险样板。

登录后查看全文
warp