CmdPal 参数页(Parameters Page)扩展 SDK 设计规范与实现指南
IParametersPage 是 Microsoft PowerToys 中 Command Palette(CmdPal)模块扩展 SDK 的一部分,它让命令在真正执行前可以要求用户补充参数(如字符串、枚举列表、文件等),从而把"一条带表单的查询"渲染进主搜索框。本文以规范草稿 ParametersPage-draft-D.md 为骨架,逐条拆解参数页的接口模型、三类参数运行单元的渲染与交互规则,并结合仓库中已落地的 IDL 接口、Toolkit 基类、UI ViewModel 与示例页源码,说明这套设计从"草稿 Addenda"演进到真实 API 的实现全貌。
文档背景:Addenda II-D 在 SDK 规范中的位置
该文档是 CmdPal 扩展 SDK 早期规范(initial-sdk-spec)下的一组"增补条款(Addenda)"草稿中的第四篇,由 Mike Griese 起草(创建于 2025-09-04,最后更新于 2025-09-08),标题为 Addenda II-D: Parameters page。它归属于 Addenda II: Commands with Parameters 这一主题分支,用于回答一个问题:
当一条命令(如"在某文件夹中创建一条笔记")除了查询词本身还需要额外输入时,扩展作者应当如何在 CmdPal 的 UI 里收集这些输入?
文档给出的答案是:引入一种新的页面类型 IParametersPage。与普通列表页不同,参数页把"输入表单"直接内嵌到 CmdPal 顶部搜索框中——用户在同一个输入框里依次填充所有参数,填齐后 CmdPal 才展示唯一的执行命令项。
与它并排的还有 RichSearchBox-draft-A.md、PrefixSearch-draft-B.md、PlainRichSearch-draft-C.md 等其它增补草稿,它们共同勾勒了搜索框演进的路线图。需要说明的是:本文讨论的"草稿"并非空中楼阁——下文中你会看到,IParameterRun 接口的 UUID(a2590cc9-510c-4af7-b562-a6b56fe37f55)、属性签名与整套 Toolkit 基类,已经逐字落地到了当前仓库的真实代码中。
核心接口模型:从 IParameterRun 到 IParametersPage
文档给出了参数页的完整接口树。首先是一个空的标记接口作为所有"参数运行单元(run)"的公共基座:
[uuid("a2590cc9-510c-4af7-b562-a6b56fe37f55")]
interface IParameterRun requires INotifyPropChanged
{
};
interface ILabelRun requires IParameterRun
{
String Text { get; };
};
interface IParameterValueRun requires IParameterRun
{
String PlaceholderText{ get; };
Boolean NeedsValue{ get; }; // TODO! name is weird
};
interface IStringParameterRun requires IParameterValueRun
{
String Text{ get; set; };
// TODO! do we need a way to validate string inputs?
};
interface ICommandParameterRun requires IParameterValueRun
{
String DisplayText{ get; };
ICommand GetSelectValueCommand(UInt64 hostHwnd);
IIconInfo Icon{ get; }; // ? maybe
};
interface IParametersPage requires IPage
{
IParameterRun[] Parameters { get; };
IListItem Command { get; };
};
把这段草稿与仓库中正式的接口定义 Microsoft.CommandPalette.Extensions.idl 对照可以发现:除了补齐 [contract(...)] 版本特性外,IParameterRun 的空接口、ILabelRun.Text 只读、IStringParameterRun.Text 读写、ICommandParameterRun.GetSelectValueCommand(UInt64) 等签名几乎逐字一致——草稿所描述的接口后来确实进入了扩展契约。
接口树的关键语义可以归纳为:
| 接口 | 父接口 | 承担角色 | 关键成员 |
|---|---|---|---|
IParameterRun |
INotifyPropChanged |
所有 run 的公共基类(标记接口) | 无 |
ILabelRun |
IParameterRun |
纯文本标签,无交互 | Text(只读) |
IParameterValueRun |
IParameterRun |
可接收值的参数基类 | PlaceholderText、NeedsValue |
IStringParameterRun |
IParameterValueRun |
自由文本输入 | Text(读写) |
ICommandParameterRun |
IParameterValueRun |
通过命令选择值(可调用命令 / 列表) | DisplayText、GetSelectValueCommand()、Icon |
IParametersPage |
IPage |
参数页整体 | Parameters(run 数组)、Command(列表项) |
其中两个设计点值得展开:
NeedsValue是驱动整个页面状态机的开关。它被注释为"TODO! name is weird",暗示这是一个语义上略微别扭、但承担核心职责的属性。后面会看到,CmdPal 的 UI 层正是以"是否还有参数NeedsValue == true"来决定要不要显示最终执行命令。IParametersPage同时是一个IPage又是一个ICommand。因为IPage requires ICommand(见 idl 第 257 行),参数页本身可以像普通命令一样被检索、导航,而其返回的Parameters数组与Command项则定义了页面的"表单 + 提交按钮"。
打开参数页后发生了什么
文档对参数页的打开行为给出了明确约定:
当我们打开一个
IParametersPage时,会在搜索框中渲染Parameters中的每一项,并把焦点移动到第一个不是ILabelRun的IParameterRun上。
也就是说,标签 run(ILabelRun)只负责把 "Create a note"、"in" 这类提示性文字渲染在搜索框中,但它们不参与焦点顺序。用户的实际输入旅程永远从第一个需要值的参数开始。每一种具体交互形态由 IParameterRun 的具体类型决定。
文档将"可接收值"的参数归纳为三类输入:字符串(strings)、可调用命令(invokable commands)、列表(lists)。其中字符串是一个特例——它不需要任何命令来赋值;而列表与可调用命令的取舍则取决于参数对象的 SelectValueCommand 具体是什么类型(详见下文 4.3、4.4 节)。列表与可调用命令是通过 SelectValueCommand 的类型来区分的。
完成的判定:什么时候"提交"命令现身
一个贯穿始终的规则是:
当所有参数的
NeedsValue都为false时,我们将只向用户展示一个条目——即Command条目。
这与真实 UI 层的 ParametersPageViewModel.ShowCommand 逻辑完全对应(见 ParametersViewModels.cs):
public bool ShowCommand =>
IsInitialized &&
IsLoading == false &&
!NeedsAnyValues()
;
NeedsAnyValues() 遍历所有 ParameterValueRunViewModel,只要还有一个参数 NeedsValue == true,执行按钮就保持隐藏。这种"值未齐不出现提交动作"的机制,天然防止了用户半途执行命令。
字符串参数(String parameters):搜索框即文本框
字符串参数是最简单的输入类型。文档规定:
它们被渲染为搜索框内的一个文本框。用户可以直接在其中输入。当用户按 Enter 或 Tab 时,焦点移动到下一个参数。
一个完整的 StringParameterRun 示例(摘自文档的 Create-note 例子)如下:
private readonly StringParameterRun _titleParameter = new StringParameterRun()
{
PlaceholderText = "Note title"
};
在真实 Toolkit 实现 StringParameterRun.cs 中,字符串参数的 NeedsValue 直接由文本内容推导:
public virtual string Text { get => _text; set { if (SetProperty(ref _text, value)) { OnPropertyChanged(nameof(NeedsValue)); } } }
public override bool NeedsValue => string.IsNullOrEmpty(Text);
也就是说,一个没有输入任何字符的字符串参数会被视为"仍需要值"。基类同时提供了便捷构造函数 StringParameterRun(string placeholderText) 与 ClearValue()(将 Text 重置为空串并通知 NeedsValue 变化)。一个值得留意的实现细节是 Toolkit 对 Value 强类型约束的校验(见 StringParameterRun.cs):如果外部向一个字符串参数的 Value 写入非字符串对象,会抛出 ArgumentException 并通过 ExtensionHost.LogMessage 记录一条错误日志。
草稿在此处留下的开放问题是 "我们是否需要一种验证字符串输入的方式?"(TODO! do we need a way to validate string inputs?)——即对输入做格式/正则校验的能力当时尚未纳入规范。
命令参数 I:可调用命令(Invokable Commands)——按钮式取值
当参数的 SelectValueCommand 是一个 IInvokableCommand 时,它被归类为"可调用命令"型参数。此时渲染形态是一个按钮:
这类参数被渲染为搜索框内的一个按钮。按钮文本是
DisplayText(若已设置),否则是PlaceholderText。若用户点击按钮,我们调用SelectValueCommand(并忽略其CommandResult)。
文档明确指出这一形态适用于 文件选择器、日期选择器、颜色选择器 等一切"需要自定义 UI 来挑选值"的场景。典型交互流程如下:
- 按钮点击(或在按钮聚焦时按下 Enter)→ 触发
SelectValueCommand,CmdPal 忽略其返回值(CommandResult在此无关紧要)。 - 扩展完成取值后,应把
NeedsValue置为false,并可选地更新DisplayText与Icon来反映所选项。 - 当焦点位于按钮上时按下 Tab,焦点前进到下一个参数。
- 若按钮处于聚焦状态时
NeedsValue被改为false,CmdPal 会自动把焦点移动到下一个参数。
这里的关键点是 GetSelectValueCommand(UInt64 hostHwnd) 的入参。因为 WinUI 3 桌面应用中文件选择器等原生对话框必须用宿主窗口句柄初始化,CmdPal 将主窗口的 hwnd 传给扩展,扩展才能正确弹出模态选择器。文档中的文件选择器示例(FilePickerCommand)清楚地展示了这一点:
public void SetHostHwnd(uint hostHwnd)
{
_hostHwnd = hostHwnd;
}
public override ICommandResult Invoke()
{
PickFileAsync();
return CommandResult.KeepOpen();
}
private async void PickFileAsync()
{
var picker = new Windows.Storage.Pickers.FileOpenPicker();
// You need to initialize the picker with a window handle in WinUI 3 desktop apps
var file = await picker.PickSingleFileAsync();
FileSelected?.Invoke(this, file);
}
注意 Invoke() 返回 CommandResult.KeepOpen()——即命令被调用后参数页保持打开,等用户在原生选择器里完成选择后,通过 FileSelected 事件把值回传给 CommandParameterRun。这印证了文档"点击按钮调用命令时忽略 CommandResult"的约定:此时按钮命令的返回值不用于导航,只用于驱动异步取值。
对应地,真实实现中凡是实现了 IRequiresHostHwnd 的命令,Toolkit 会在 CommandParameterRun.GetSelectValueCommand 内注入宿主窗口句柄(见 CommandParameterRun.cs)。
命令参数 II:列表命令(List Commands)——搜索框 + 下拉列表
当参数的 SelectValueCommand 是一个 IListPage 时(静态列表与动态列表行为一致),它被归类为"列表命令"型参数。这是最复杂的交互形态,也是参数页真正发挥威力的地方:
这类参数被渲染为搜索框内的一个文本框。当用户聚焦该文本框时,我们会在 CmdPal 的主体区域展示
IListPage中的条目。用户随后可以输入文字来过滤列表。此过滤行为与 CmdPal 中任何其它列表页一致——CmdPal 会过滤静态列表,或者把查询传给动态列表。
这条规则揭示了参数页与普通列表页的深层关系:一个参数可以"借用"一个列表页作为其取值来源,用户在当前参数上的打字输入会被同时用作列表过滤关键字。同时,列表中的条目应为带 IInvokableCommand 的 IListItem;文档特别警告:
这些列表中的条目都应该是带
IInvokableCommand的IListItem对象。把IPage塞进这些条目会让用户导航离开参数页,这很可能是意料之外的。
在参数页语境下,列表项命令绝不能返回"导航"语义——例如在"选择文件夹"时,如果点某一项就把用户带到别处,参数页交互就断掉了。列表型参数的正确闭环如下:
- 用户聚焦该参数文本框 → CmdPal 主体显示列表项,用户打字过滤。
- 用户从列表选中一项 → 扩展应通过向
CommandRun冒泡事件来处理该命令,同时设置Value、DisplayText、Icon,并把NeedsValue置为false。 - 文本框聚焦时按 Enter → 触发列表中被选中项的命令。
- 按 Tab → 焦点前进到下一个参数。
- 聚焦期间若
NeedsValue变为false→ CmdPal 自动把焦点移动到下一个参数。
完整示例:把规范落到代码
文档给出的压轴示例是一条带两个参数的完整命令——Create a note ${title} in ${folder}:title 是字符串输入,folder 是一个静态文件夹列表。扩展作者需要定义一个 IParametersPage,其中依次排列四个 run:
- 一个
ILabelRun,文本为 "Create a note" - 一个
IStringParameterRun,用于接收title - 一个
ILabelRun,文本为 "in" - 一个
ICommandParameterRun,用于接收folder;其Command是一个IListPage,列表项为可选文件夹
文档给出的完整代码是对理解"四个 run 如何协同 + 事件如何回流"的最佳教材,原样摘录如下:
public interface IRequiresHostHwnd
{
void SetHostHwnd(UInt64 hostHwnd);
}
public sealed partial class CommandParameterRun : BaseObservable, ICommandParameterRun
{
public virtual string DisplayText { get; set; } // basic projected properties here, same as throughout the toolkit
public virtual string PlaceholderText { get; set; } // basic projected properties here, same as throughout the toolkit
public virtual ICommand Command { get; set; } // basic projected properties here, same as throughout the toolkit
public virtual IIconInfo Icon { get; set; } // basic projected properties here, same as throughout the toolkit
public virtual bool NeedsValue => Value == null; // Toolkit helper: does this parameter need a value?
public virtual ICommand GetSelectValueCommand(UInt64 hostHwnd)
{
if (Command is IRequiresHostHwnd requiresHwnd)
{
requiresHwnd.SetHostHwnd(hostHwnd);
}
return Command;
}
public object? Value { get; set; } // Toolkit helper: a value for the parameter
}
public sealed partial class CreateNoteParametersPage : ParametersPage
{
private readonly SelectFolderPage _selectFolderPage = new SelectFolderPage();
private readonly StringParameterRun _titleParameter = new StringParameterRun()
{
PlaceholderText = "Note title"
};
private readonly ICommandParameterRun _folderParameter = new CommandParameterRun()
{
PlaceholderText = "Select folder",
Command = _selectFolderPage
};
private readonly List<IParameterRun> _parameters;
private readonly CreateNoteCommand _command = new() { TitleParameter = _titleParameter, FolderParameter = _folderParameter };
private readonly ListItem _item = new(_command);
public IParameterRun[] Parameters => _parameters.ToArray();
public IListItem Command => _item;
public CreateNoteParametersPage()
{
_parameters = new List<IParameterRun>
{
new LabelRun("Create a note"),
_titleParameter,
new LabelRun("in"),
_folderParameter
};
_selectFolderPage.FolderSelected += (s, folder) =>
{
_folderParameter.Value = folder;
_folderParameter.Icon = folder.Icon;
_folderParameter.DisplayText = folder.Name;
};
};
}
这个 CreateNoteParametersPage 至少示范了三条关键工程惯例:
- 参数与命令通过强类型引用互相绑定:
CreateNoteCommand内部用TitleParameter/FolderParameter属性引用页面中的 run 实例(internal ... { get; init; } // set by the parameters page),命令执行时直接读取参数当前值,无需自行维护副本。 - 参数页不直接碰业务:选中文件夹的
FolderSelected事件处理器只负责"镜像"取值结果到_folderParameter(Value、Icon、DisplayText),列表的选择与过滤完全由 CmdPal 托管。 Command是一个普通的ListItem:页面只负责在恰当时机把它交给 CmdPal,由 UI 层决定何时展示。
接着是消费这些参数的命令实现。注意 Invoke() 内部对字符串与列表参数采取不同的校验/处理策略:
public sealed partial class CreateNoteCommand : BaseObservable, IInvokableCommand
{
internal IStringParameterRun TitleParameter { get; init; } // set by the parameters page
internal ICommandParameterRun FolderParameter { get; init; } // set by the parameters page
public IIconInfo Icon => new IconInfo("NoteAdd");
public override ICommandResult Invoke()
{
var title = TitleParameter.Text;
if (string.IsNullOrWhiteSpace(title))
{
var t = new ToastStatusMessage(new StatusMessage(){ Title = "Title is required", State = MessageState.Error });
t.Show();
return CommandResult.KeepOpen();
}
var folder = FolderParameter.Value;
if (folder is not Folder)
{
// This is okay, we'll create the note in the default folder
}
// Create the note in the specified folder
NoteService.CreateNoteInFolder(title, folder); // whatever your backend is
return CommandResult.Dismiss();
}
}
这里有一个值得注意的细节:即便所有参数页机制都正常工作,Invoke() 内仍保留了二次防御——标题为空时弹出错误 Toast 并 KeepOpen(),让用户留在页面上补全。这暗示 NeedsValue 只是 UI 层的"放行闸门",业务校验仍由命令负责。
最后是作为取值来源的文件夹列表页,以及它如何通过事件把选中值"传回"参数页:
public sealed partial class SelectFolderPage : ListPage
{
public event EventHandler<Folder>? FolderSelected;
public SelectFolderPage()
{
// Populate the list with folders
var folders = FolderService.GetFolders(); // whatever your backend is
Items = folders.Select(f => new ListItem(new SelectFolderCommand(f), f.Name, f.Icon)).ToArray();
}
private sealed partial class SelectFolderCommand : BaseObservable, IInvokableCommand
{
private readonly EventHandler<Folder> _folderSelected;
private readonly Folder _folder;
public IIconInfo Icon => _folder.Icon;
public string Title => _folder.Name;
public SelectFolderCommand(Folder folder, EventHandler<Folder> folderSelected)
{
_folder = folder;
_folderSelected = folderSelected;
}
public override ICommandResult Invoke()
{
_folderSelected?.Invoke(this, _folder);
return CommandResult.KeepOpen();
}
}
}
这段代码再次印证了 4.4 节的规则:列表项命令 SelectFolderCommand.Invoke() 返回 CommandResult.KeepOpen(),而不是任何会导致导航的结果——它仅仅把选中的文件夹通过 FolderSelected 事件冒泡出去,让参数页构造函数中注册的处理器去回填参数。
示例同时给出了一个可复用的文件选择参数子类 FilePickerParameterRun,把"文件对话框 + 参数回填"封装成 20 余行的成品,你可以在自己的扩展里直接仿写:
public sealed partial class FilePickerParameterRun : CommandParameterRun
{
public StorageFile? File { get; private set;}
public FilePickerParameterRun()
{
var command = new FilePickerCommand();
command.FileSelected += (file) =>
{
File = file;
if (file != null)
{
Value = file;
DisplayText = file.Name;
// Icon = new IconInfo("File");
}
else
{
Value = null;
DisplayText = null;
// Icon = new IconInfo("File");
}
};
PlaceholderText = "Select a file";
Icon = new IconInfo("File");
Command = command;
}
// ... FilePickerCommand 实现见上文"可调用命令"一节
}
再往下,文档提供了一套类型安全的静态列表工具类,它们把"泛型值集合 → 列表页 → 值回传事件"这条流水线通用化,值得完整阅读(见 ParametersPage-draft-D.md):
public sealed partial class SelectParameterCommand<T> : InvokableCommand
{
public event TypedEventHandler<object, T>? ValueSelected;
private T _value;
public T Value { get => _value; protected set { _value = value; } }
public SelectParameterCommand(T value) { _value = value; }
public override ICommandResult Invoke()
{
ValueSelected?.Invoke(this, _value);
return CommandResult.KeepOpen();
}
}
public sealed partial class StaticParameterList<T> : ListPage
{
public event TypedEventHandler<object, T>? ValueSelected;
private bool _isInitialized = false;
private readonly IEnumerable<T> _values;
private readonly List<IListItem> _items = new List<IListItem>();
private Func<T, ListItem, ListItem> _customizeListItemsCallback;
// ctor takes an IEnumerable<T> values, and a function to customize the ListItem's depending on the value
public StaticParameterList(IEnumerable<T> values, Func<T, ListItem> customizeListItem)
{
_values = values;
_customizeListItemsCallback = (value, listItem) => { customizeListItem(value); return listItem; };
}
public StaticParameterList(IEnumerable<T> values, Func<T, ListItem, ListItem> customizeListItem)
{
_values = values;
_customizeListItemsCallback = customizeListItem;
}
public override IListItem[] GetItems()
{
if (!_isInitialized)
{
Initialize(_values, _customizeListItemsCallback);
_isInitialized = true;
}
return _items.ToArray();
}
private void Initialize(IEnumerable<T> values, Func<T, ListItem, ListItem> customizeListItem)
{
foreach (var value in values)
{
var command = new SelectParameterCommand<T>(value);
command.ValueSelected += (s, v) => ValueSelected?.Invoke(this, v);
var listItem = new ListItem(command);
var item = customizeListItem(value, listItem);
_items.Add(item);
}
}
}
这份工具代码的用途是:扩展作者只需要 new StaticParameterList<Folder>(folders, f => new ListItem(...)),即可得到一个会自动为每个值生成"选中命令"并广播 ValueSelected 事件的静态列表页——参数页业务代码从此不必手写逐条的 SelectFolderCommand。
从草稿到 API:仓库中的真实实现与它们的行为细节
IDL 层:契约正式落地
参数页全套接口已在扩展契约 Microsoft.CommandPalette.Extensions.idl 中以 ExtensionsContract v1 版本正式发布。草稿中的开放问题(如 ICommandParameterRun.Icon 后的 // ? maybe)在落地时被保留了下来,接口形态与草稿基本一一对应,唯一明显的补充是每个接口都被标注了 [contract(...)] 以便做 ABI 版本演进管理。
Toolkit 层:给扩展作者的开箱基类
Microsoft.CommandPalette.Extensions.Toolkit 命名空间为每个接口提供了可继承的偏类型实现,文件集中在 Parameters 目录下:
- ParametersPage.cs:抽象基类,要求派生类只实现
Command与Parameters两个成员,其余页面行为(标题、加载状态等)继承自Page。 - ParameterValueRun.cs:所有可取值参数的抽象基类,定义
PlaceholderText,并把Required布尔值作为NeedsValue的兜底来源:
public virtual bool NeedsValue => _required;
private bool _required = true;
public virtual bool Required
{
get => _required;
set { _required = value; OnPropertyChanged(nameof(NeedsValue)); }
}
该基类注释里写明了这一"多态覆盖"约定:默认情况下参数在 Required 为真时始终"需要值";派生类型(如 StringParameterRun)通过重写 NeedsValue getter 引入自身状态(文本是否为空、值是否为 null)。它还要求派生类实现抽象的 Value { get; set; } 与 ClearValue()。
- StringParameterRun.cs 与 LabelRun.cs:如前述,字符串参数以
string.IsNullOrEmpty(Text)计算NeedsValue;标签只需一个LabelRun(string text)构造函数即可创建。 - CommandParameterRun.cs:以
Value == null计算NeedsValue,负责在GetSelectValueCommand中按需把hostHwnd注入到实现了IRequiresHostHwnd的命令,并在Value变化时同步触发NeedsValue的通知。
一个容易踩坑的语义是:Toolkit 中 CommandParameterRun.Value 的 setter 同时会触发 NeedsValue 的 OnPropertyChanged(见 CommandParameterRun.cs),而 StringParameterRun 则在 Text 变化时做同样的事——这正是 4.2/4.3/4.4 中"值一旦就绪、CmdPal 自动推进焦点"能实现的底层原因(属性通知沿 PropChanged → ViewModel 一路传导)。
UI 层:ParametersPageViewModel 如何驱动交互
参数页的交互编排集中在 ParametersViewModels.cs。这个文件回答了"接口定义好之后,CmdPal 主程序如何把草稿描述的交互落实":
- 按类型分发(L651-L677):
FetchItems()对IParametersPage.Parameters数组中的每个对象做模式匹配,ILabelRun→LabelRunViewModel、IStringParameterRun→StringParameterRunViewModel、ICommandParameterRun→CommandParameterRunViewModel,无法识别的类型记录错误日志。这正是草稿中"交互取决于IParameterRun的类型"的实现处。 - 按钮 vs 文本框的切换(L356-L371):
CommandParameterRunViewModel以_listViewModel != null判定自己是否为"列表参数"(对应草稿中SelectValueCommand是IListPage还是IInvokableCommand)。对于列表参数,它暴露ShowTextBox => NeedsValue || IsEditing——参数还没值或用户正在重新挑选时显示文本框,否则以ButtonLabel(即DisplayText)渲染为按钮。BeginEditing()/CancelEditing()对应"用户已持有值但想重新选择"的场景。 - host 窗口句柄注入(L433-L448):初始化时通过
WeakReferenceMessenger发送GetHwndMessage取回 CmdPal 主窗口句柄,再调用commandRun.GetSelectValueCommand((ulong)msg.Hwnd);若返回的是IListPage则包装成ListViewModel(复用普通列表页的过滤/渲染管线,正是草稿中"与任何其它列表页一致"的实现),若是IInvokableCommand则包装成CommandViewModel。 - 焦点推进(L850-L882):
FocusNextParameter(lastParam)在剩余参数里寻找下一个仍NeedsValue的项并通过FocusParamMessage让 UI 聚焦;找不到时回落到第一个未填值参数。列表参数的值一旦确认(扩展方回调触发ValueChanged),页面便调用SetActiveListParameter(null)收起列表、FocusNextParameter推进焦点(L796-L822)。 - 命令显现时机:
UpdateCommand()只在ShowCommand(即!NeedsAnyValues())为真时通过UpdateCommandBarMessage把Command发布给 UI;用户可通过TrySubmit()触发最终执行(L841-L848)。
这些机制在 ParametersPageViewModelTests.cs 中配套了单元测试,扩展作者若想核对诸如"填齐参数后 ShowCommand 才为真""值变化后焦点是否正确前移"等行为,可以从测试用例入手反向确认预期。
仓库内的真实示例页
规范不只停留在文档层,仓库的示例扩展提供了可直接运行的三组参数页(SamplePagesExtension/Pages/ParameterSamples.cs):
SimpleParameterTest:最简字符串参数页——一个LabelRun("Enter a value:")加一个StringParameterRun(PlaceholderText = "Type something"),提交后弹出 Toast 并调用_stringParameter.ClearValue()复位。ButtonParameterTest:按钮式文件选择——一个FilePickerParameterRun加前后两个标签,提交时从_fileParameter.Value取StorageFile并打开(对应 4.3 节"文件选择器"用例)。MixedParamTestPage:混排示例,同一页面同时承载字符串参数与文件选择参数,并且通过构造函数参数stringFirst控制字符串参数与文件参数谁先出现——可用于验证焦点推进对参数顺序的敏感性。
将这三页与草稿中的 CreateNoteParametersPage 对照,可以看到从"规范示例"到"示例扩展"的演化:页面都遵循"持有参数对象 → 提交命令读取 Value/Text → Toast 反馈 → ClearValue() 复位"的统一模式。
早期的另一条路线:任意参数与动作链探索
文档中 ## original draft starts here 之后的段落,是参数页定稿之前的一段更早思路记录(Mike 标注了"以下是原始笔记,不属于当前 SDK 规范")。这段内容对理解"为什么最终选择 run 流式模型"很有价值,也代表了被搁置的另一套候选设计。
第一版设想面向"任意参数直接挂在命令上":
enum ParameterType
{
Text,
File,
Files,
Enum,
Entity
};
interface ICommandParameter
{
ParameterType Type { get; };
String Name { get; };
Boolean Required{ get; };
// TODO! values for enums?
// TODO! dynamic values for enums? like GetValues(string query)
// TODO! files might want to restrict types? but now we're a file picker and need that whole API
// TODO! parameters with more than one value? Like,
// SendMessage(People[] to, String message)
};
interface ICommandArgument
{
String Name { get; };
Object Value { get; };
};
interface IInvokableCommandWithParameters requires ICommand {
ICommandParameter[] Parameters { get; };
ICommandResult InvokeWithArgs(Object sender, ICommandArgument[] args);
};
随后作者意识到"可枚举的参数类型表"很快会不够用,于是在 TODO 里提出增加 CustomPicker 类型,让扩展自带选择器 UI:
我们应该添加类似
CustomPicker的参数类型,允许扩展为参数定义自己的选择器。当需要填充参数值时,我们调用类似ShowPickerAsync(ICommandParameter param)的东西,让扩展去填值,我们不关心值是什么。
于是第二版草稿演化成"参数即对象"(参数自己携带值、展示名、图标与取值的 ShowPicker 方法):
enum ParameterType
{
Text,
// File,
// Files,
Enum,
Custom
};
interface ICommandArgument requires INotifyPropChanged
{
ParameterType Type { get; };
String Name { get; };
Boolean Required{ get; };
Object Value { get; set; };
String DisplayName { get; };
IIconInfo Icon { get; };
void ShowPicker(UInt64 hostHwnd);
};
interface IInvokableCommandWithParameters requires ICommand {
ICommandArgument[] Parameters { get; };
ICommandResult InvokeWithArgs(Object sender, ICommandArgument[] args);
};
值得注意的是 enum 的可选值列表(GetValues())与文件多选、批量类型参数(如 SendMessage(People[] to, String message))等场景,在两版草稿中始终被标记为 TODO 或注释掉——它们都涉及更重的 UI 与数据约定,作者明确选择不在 v1 硬啃。
作者还记录了一个更大胆的"实体链"设想(注记块中被称为 Quicksilver 式 "thing, do" 流程):让命令返回 CommandResult.Entity,把实体留在查询框顶部作为"徽章",随后只展示能接受该实体的命令,从而支持 文件 → 移除背景、发送到 Teams 聊天 这类连续动作。但这个设想因为两个现实问题被推迟:一是对第三方 Action Framework 的消费侧行为缺乏可见度,无法精确建模;二是像"移除背景"这类页面需要更灵活的"任意实体 +"按钮而非死板的参数表。
作者最终的判断被忠实记录在文档末尾,这也是理解参数页 v1 边界的关键决策:
把动作链起来听起来很有意思,但我决定把它排除在官方 v1 规范之外。没有它我们也能交付一个可用的 DevPal v0.1,之后再加。
也就是说,当前文档正文的 IParametersPage 模型是"能立刻落地"的收敛方案,而"任意参数/动作链"则留作 v1 之后的演进方向。
扩展作者实践要点小结
- 选择正确的参数类型:纯文本用
StringParameterRun;需要系统级自定义选择器(文件、日期、颜色)用IInvokableCommand型CommandParameterRun,并实现IRequiresHostHwnd以接收hostHwnd;需要"从集合中挑一个"用IListPage型参数,页面主体会自动变成可过滤列表。 - 牢记
NeedsValue的驱动语义:所有参数NeedsValue都为假时Command才出现。StringParameterRun以文本非空判定、CommandParameterRun以Value != null判定(见 StringParameterRun.cs 与 CommandParameterRun.cs),务必在取值完成后让二者进入"满足"状态,否则提交按钮永不出现。 - 列表项命令只能
KeepOpen并事件回传:不要在参数列表项中返回任何会触发页面导航的命令结果;正确姿势是通过FolderSelected/ValueSelected类事件把选中值写回参数对象的Value/DisplayText/Icon。 - 命令内仍需业务校验:
NeedsValue只约束 UI 门禁;像CreateNoteCommand那样在Invoke()里对空标题做 Toast 兜底校验并返回KeepOpen(),是防御推荐写法。 - 复用 Toolkit 与示例:可直接继承 ParametersPage.cs、使用 ParameterSamples.cs 中的三页样例起步;页面 UI 层行为细节可对照 ParametersViewModels.cs 与 ParametersPageViewModelTests.cs 验证。
参数页的设计把"命令执行前的数据收集"压缩进了搜索框本身——字符串、按钮、列表三类 run 以最小的认知负担覆盖了绝大多数取数场景,而其与列表页深度复用的架构,也让它在实现上保持了克制的复杂度。若你想追踪这套设计的后续演化(例如动作链与 CustomPicker 是否被重新提起),可继续阅读主规范 initial-sdk-spec.md 中 Addenda II 及其余增补条款。
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 StartedRust0624
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