VS Code 会话布局控制器(手机布局):MobileLayoutController 规则 M1–M2 规格与源码解析
本文以仓库中会话布局子系统的「手机布局控制器」规格文档 mobileSessionLayoutController.md 为主体,结合其基类规范、注册入口源码与单元测试,解析 VS Code 在 web phone(网页手机)布局下如何管理每个会话(session)的界面状态。读完后你将掌握:布局控制器家族的 B/D/M 规则体系如何组织、手机控制器为什么"刻意不管"侧栏(auxiliary bar)、以及 M1/M2 两条规则分别如何被代码与测试落实。
一、定位:三份规格、三套控制器
VS Code 的会话布局(Sessions Layout)体系按使用形态拆成三类控制器,每一类对应一份 Spec 文档,规则带稳定引用标签,供源码与测试引用:
| 控制器 | 规则前缀 | 覆盖布局 | 规格文档 |
|---|---|---|---|
BaseLayoutController(抽象基类) |
[B*] |
所有布局共享的逐会话布局状态 | baseSessionLayoutController.md |
LayoutController(桌面控制器) |
[D*] |
桌面端与 web desktop 布局 | desktopSessionLayoutController.md |
MobileLayoutController(手机控制器) |
[M*] |
web phone 布局 | mobileSessionLayoutController.md |
其中手机控制器是一个"裁剪版"控制器:它继承 BaseLayoutController(从而拿到 B1–B6 全部共享行为),但刻意省略了辅助栏(auxiliary bar,即侧栏/secondary side bar)的可见性管理。理由很直白——窄视口上如果侧栏被自动展开,会对用户造成破坏性干扰。
如果对整个规则家族感兴趣,可先读文件级总纲 LAYOUT_CONTROLLER.md(基类规范在其开头自述为该文件的"file-level companion")。
阅读约定:规格文档中的 Rules 描述用户可见行为、按场景分组,每个规则带稳定标签(
[M*]/[B*]/[D*]),编号不代表优先级或执行顺序;Implementation notes 则描述规则的实现方式,只有改代码时才需要读。此外每份规格文档都带一个 Specification change gate 提示:恢复既有规则行为的 bug 修复应落在回归测试里,仅当预期的控制器行为发生改变时才更新本文档。
二、注册:只在 web phone 布局下生效
手机控制器并非所有平台都会装载。查看注册贡献 sessions.layout.contribution.ts 的构造逻辑,可以清楚看到选择分支:
if (layoutService.isSinglePaneLayoutEnabled) {
this._register(instantiationService.createInstance(SinglePaneLayoutController));
return;
}
if (isWeb && isMobile) {
this._register(instantiationService.createInstance(MobileLayoutController));
return;
}
this._register(instantiationService.createInstance(LayoutController));
也就是说,在满足 isWeb && isMobile(web 平台 + 移动设备,即手机浏览器)时实例化 MobileLayoutController;其余所有布局(桌面端、web desktop,以及未开启 single-pane 的形态)走桌面控制器 LayoutController;若启用了 single-pane 详情节流板布局,则优先交给 SinglePaneLayoutController。
这一点在手机控制器的 实现注解 中也有对应描述,并明确补充两点:
- 该贡献由 sessions.web.main.ts 导入——它在注释里写明 "The web bundle serves both the web desktop and the web phone layouts; the layout contribution registers the correct one at runtime.",即同一份 web 构建同时服务两种布局,控制器在运行时按环境选择;
- 注意规格文档把贡献阶段标注为
WorkbenchPhase.AfterRestored,而当前源码(sessions.layout.contribution.ts 第 44 行)实际以WorkbenchPhase.BlockRestore注册该 workbench contribution——判断时以实际代码为准。
三、场景一「手机上的布局」与规则 M1:与其他 surface 完全相同的逐会话布局
规格文档把 M1 归纳为一句话:会话仍然像其它任何 surface 一样记住自己的布局。手机只是没有侧栏的容身之处,但布局记忆能力不被削弱。M1 的具体含义需要回落到基类规范 baseSessionLayoutController.md 中被它逐条引用的 B 规则:
- B1 — 面板可见性(Panel visibility):底部面板(bottom panel)的显示/隐藏按会话记忆,默认隐藏;切换会话时恢复该会话上次的状态。开启 single-pane 时会禁用这种"逐会话"作用域(面板改由 workbench 级别管理),但手机布局保持基类默认的逐会话管理。
- B2 — 打开的编辑器(Open editors):每个会话在激活时恢复自己的一组打开的编辑器;切换会话会先保存离开的会话、再套用目标会话。新会话/未命名会话以及无保存记录的会话,绝不会强制撑开或清空编辑器区。
workbench.editor.useModal为'all'时浏览器编辑器仍停靠共享的 grid editor part,因此该模式下编辑器标签页依旧需要逐会话捕获/恢复。 - B3 — 启动时恢复:应用启动时恢复每个会话保存的布局;损坏数据被防御性忽略,并一次性从旧版
sessions.workingSets键迁移。 - B4 — 关闭时保存:关闭或重载应用时,通过
IStorageService.onWillSaveState钩子把每个会话当前布局写入sessions.layoutState(StorageTarget.MACHINE)。 - B5 — 多会话回退默认:同一时刻可见会话多于一个时,暂停逐会话的面板恢复,并丢弃这些可见会话记忆的面板/辅助栏状态,从而折叠回单会话时显示默认布局;但打开的编辑器工作集仍被保留。
从源码上看,这些行为全部集中在基类 baseSessionLayoutController.ts 中,以 ResourceMap<...> 按会话资源(URI)存储:_panelVisibilityBySession、_panelViewBySession、_workingSets、_editorPartHiddenBySession、_viewStateBySession 等,并统一序列化进工作区级存储键 sessions.layoutState。手机控制器不触碰这套机制,因此 M1 天然成立——这也是"手机布局复用共享的逐会话布局状态"的直接体现。
四、规则 M2:侧栏(辅助栏)永不自动化
M2 是手机控制器区别于桌面控制器的关键规则。规格文档将其表述为:
The auxiliary bar (side pane) is never auto-opened, auto-closed or remembered on phones — not when switching sessions, submitting a new session, or when changes arrive.
也就是说,无论以下哪种时刻发生,手机端都不会自动开/关侧栏,也不会把侧栏状态记入某个会话:
- 切换会话时;
- 提交(submit)一个新会话时;
- 会话产生新的文件变更(changes)时。
这样做的目的,是为了避免在手机窄视口上破坏性地自动展开 secondary side bar。反观桌面控制器 desktopSessionLayoutController.md,其 D1–D11 规则全部围绕辅助栏的记忆、恢复、默认视图(Files/Changes)、空栏隐藏与响应式折叠展开,两者形成鲜明对照:桌面控制器负责"管好侧栏",手机控制器负责"完全不碰侧栏"。
五、实现注解:刻意不覆盖 _registerViewStateManagement
规格文档的 Implementation notes 部分揭示了 M2 的代码级实现手法,且 mobileSessionLayoutController.ts 的完整源码只有一句话的分量:
export class MobileLayoutController extends BaseLayoutController {
static readonly ID = 'workbench.contrib.sessionsMobileLayoutController';
// [M2] Intentionally does not override `_registerViewStateManagement`, so the
// auxiliary bar is never auto-shown / hidden / captured on phone viewports.
}
理解这段"空实现"的意义,需要回到基类的钩子设计。基类构造函数在 baseSessionLayoutController.ts 末尾调用三个可被子类覆写的钩子:
_registerViewStateManagement()—— 用于平台特定的辅助栏接线;基类为空实现,桌面控制器覆写它来挂上 D1/D2/D3/D5/D7 逻辑,而手机控制器刻意不覆写;_captureActiveSessionViewState(resource)—— 供 B4 保存时调用的快照钩子,基类为空实现;_registerAuxiliaryControllers()/_onSidePaneToggled()等 —— 同样留空或默认无操作。
手机控制器的"不覆写"正是 M2 的实现:基类构造函数确实定义了"逐会话 view state(辅助栏可见性与激活容器)"的存储映射 _viewStateBySession 和相关的 autorun/监听逻辑,但在 _isViewStatePerSession 等开关全为基类默认值的前提下,没有桌面控制器去解释 ISessionViewState(桌面端将其视为不透明持久化数据并覆写钩子消费它),这些状态就永远不会被写入或用于驱动辅助栏的显隐。因此没有任何一份桌面端辅助栏逻辑会在手机布局上运行。
六、测试验证:M1/M2 的守门人
规格的行为不是靠文档自证,而是由测试兜底。见 mobileSessionLayoutController.test.ts,它显式用测试标题标注规则标签:
[M2] does not manage the auxiliary bar for untitled or existing sessions:分别构造未命名会话(桌面控制器此时会打开 Files 视图)和已存在会话(桌面控制器此时会隐藏辅助栏),断言openedViews与openedViewContainers始终为空、setPartHiddenCalls中没有任何针对Parts.AUXILIARYBAR_PART的调用——即既不开视图、也不开容器、更不切换辅助栏可见性;[M2] ignores auxiliary bar maximize events:即使触发编辑器最大化事件,也断言openedViews保持为空——桌面端 D5"最大化时侧栏固定显示 Changes"的逻辑在手机上绝不生效;[M1] still hides the panel by default (base behaviour):切换会话时,面板被默认隐藏(Parts.PANEL_PART+hidden === true),验证从基类继承的 B1 默认值依然工作。
这些测试通过 createTestHarness(见同目录 layoutControllerTestUtils.ts)模拟会话、activeSession 可观察对象与部件可见性调用,从而在无 UI 环境下验证控制器行为。开发者在为手机控制器做任何改动时,都应保证这些标了 [M*] 的用例继续通过——这正对应规格文档开头的 "Specification change gate"。
七、如何判断该看哪一份文档
在会话布局子系统中继续深入时,可按下表定位:
- 你在手机/移动 web 上研究布局、想了解"为什么手机端不自动展开侧栏"→ 读 mobileSessionLayoutController.md,看规则 M1–M2;
- 你想了解所有布局共享的逐会话记忆(面板、编辑器工作集、持久化、多会话回退)→ 读基类规范 baseSessionLayoutController.md 的 B1–B6,并对照 baseSessionLayoutController.ts;
- 你在桌面/web desktop 上研究辅助栏与侧栏的完整行为(Files/Changes 默认视图、最大化、响应式折叠)→ 读 desktopSessionLayoutController.md 的 D1–D11;
- 你想知道某个控制器在什么条件下被装载 → 看注册分支 sessions.layout.contribution.ts 与对应 main 入口(桌面走 sessions.desktop.main.ts,web 走 sessions.web.main.ts)。
八、小结
MobileLayoutController 是"少即是多"的典型实现:它不新增任何属于自己的行为代码,仅通过继承基类的全部逐会话布局能力(M1 → B1/B2/B3/B4/B5)与拒绝覆写辅助栏接线钩子(M2),就在网页手机布局上同时保证了会话布局的跨会话、跨重启记忆,以及窄视口下侧栏的绝对安静。理解它,等于同时理解了会话布局控制器家族"基类共享 + 平台子类增量"的整体架构哲学——这也正是三份配套 Spec 文档(B*/D*/M*)想向阅读者传达的设计骨架。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00