Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,6 @@

- 兜底值的单一 owner 与失败归因:当同一个业务值(工作目录、根路径、身份、超时)既被**预备阶段**(草稿 / 预览 / 校验)消费、又被**执行阶段**(提交 / 启动 / 发送)消费时,它必须由同一个 resolver 从同一个请求源解析。触发条件:两阶段各自算一遍,且其中一个带兜底(如 stdio 缺目录时退到用户主目录)而另一个不带。原生期望行为是兜底规则连同其**分类依据**(此处为传输类型)一起收进那个 resolver,调用方只传原始请求值与分类依据。**此类缺陷最隐蔽的形态是「预备阶段成功、执行阶段失败」**:草稿能建、按钮亮着、选择器认为选择合法,用户点下去才失败,且失败文案是「请稍后重试」——而重试永远不会让缺失的值长出来。因此不可重试的失败必须呈现 resolver 自己的原因(「请先选择远程目录」而非「请稍后重试」),并且**不得只靠一道守卫**:既有的选择器/命令门禁拦住的是它能判定的子集,resolver 守卫是第二道,二者断言不同契约、不可互相替代。禁止做法:为「不是真实实体」的合法哨兵值(未归类 / 全部 / 默认)返回 null 再让下游当错误;在执行阶段新写一份兜底;把不可重试失败呈现为可重试;留下第二个 resolver 并让其唯一调用点传常量参数(它退化成 Trim 包装,看起来仍是「架构层」)。验证覆盖:哨兵值 + 各传输类型的组合各跑一遍(本地兜底必须成功启动、远程必须拒绝并给出原因);**反向验证必须把缺陷种回去**并确认新增测试见红(仅新测试全绿不构成证据);删除第二个 owner 时以 fail-closed 门禁断言其文件不存在且执行阶段只出现唯一 resolver 调用;以及审计**所有**触达执行阶段的既有测试——它们可能把缺陷行为写成断言(如 `Assert.Null(cwd)`),也可能因为夹具从未配置分类依据而在守卫前 return,使 `await 某个只在下游置位的 TaskCompletionSource` 永久挂死:表现为整套测试**无任何失败输出地超时**,而单跑新测试却是秒级全绿。
- 测试进程级监听器与可释放资源隔离:当测试创建会注册进程级 listener、provider、event source 或其它全局回调的 `IDisposable` 对象时,每个用例必须在自身作用域内确定释放。原生期望行为是用例结束后全局注册恢复到进入前的状态,后续用例的采样、事件与回调只由自身夹具决定;禁止依赖串行执行、GC、终结器、平台速度、重试或固定延时清理泄漏,也禁止把多个 live listener 聚合后的全局标志误当成单个 provider 的裁决。验证覆盖同一测试类完整顺序、多轮进程级重复运行和完整程序集;诊断时用受控的共存 listener 量化污染率,并在交付前删除临时探针。
- 入站交互的应答与展示所有权:当 agent 请求需要用户输入,而 UI 无法展示、只承载一张表单的 UI 已有未答表单或响应发送失败时,请求必须由同一 pending owner 收敛;不可展示的请求按协议允许的取消结果应答,连接仍有效时已展示表单保留到成功响应。连接仍有效时发送成功才移除 pending,失败允许重试,取消意图不得因在途发送失败而丢失;清理同时核对请求实例与连接归属,URL completion 按连接关联并忽略未知或已完成 ID。禁止静默丢弃、覆盖未答表单、按会话或复用 ID 清除新请求、用按钮状态代替协议层单次应答门控。验证覆盖真实传输响应、无 sessionId、未知必填字段、同时到达的表单、发送失败与取消竞态、断连重连和 ID 复用。
- 短命令的管道与进程收尾:当版本探测或平台授权服务启动重定向 stdout/stderr 的命令时,共享 runner 必须并发排空两条流、限制保留输出,并在调用方取消或超时时终止仍归属该进程树的命令、限时等待退出、观察所有读取任务;调用方取消时,清理失败保留在取消异常的内层原因中。禁止只读一条流、用 Process.Dispose 代替终止、让清理异常改写取消语义,或声称退出父进程的 PID 能回收已被系统重新托管的子进程。验证使用真实管道覆盖超出容量的双流输出、调用方取消、超时、终止失败及父退出后子进程持有管道;夹具记录并回收自身 PID,不按进程名清理。
- 安装器拥有命令的 PATH 注册:当同一产品既分发 GUI 又分发命令行入口时,命令必须由**每个安装包**用该平台安装器自己的机制注册(MSIX 的 `windows.appExecutionAlias`、MSI 的 `Environment` 表、dpkg 拥有的符号链接、pkg 的 postinstall),且命令二进制必须由单一 publish 脚本产出后被各安装包嵌入。禁止做法:脚本编辑 `.bashrc` / `.zshrc` / 用户 PATH 字符串或调 `setx`(安装、升级、卸载必须同属一个所有者);每条打包链各自构建命令二进制(四份产物都能构建成功,正是它们开始漂移的形态);把注册断言写成只在打 tag 时才跑的内联脚本。验证方式:注册必须对**本次构建产出的安装物**断言(读包内清单、`Environment` / `File` 表、文件列表),断言规则要与读取产物的平台 API 分离,使其正反例能在任意平台的每次 push 上跑;能真装真卸的平台必须有安装→命令解析→卸载→残留检查全链门禁;符号链接必须断言**解析结果**而非链接文本(相对深度错误只在解析时暴露,包的文件列表看不出来);平台把辅助可执行文件放进包内哪个目录若非文档化契约,消费方必须探测候选位置并报告命中项,禁止赌一个位置。唯一例外是**该平台的包格式没有安装钩子**(macOS 拖拽 .app):此时应用自身可以拥有该注册,但必须写与该平台安装器**同一路径、同一形状**(先删再链),必须同时提供移除入口,且该逻辑要拆成可在任意平台用假根跑的脚本/服务 —— 应用与安装器各写一套是同一个双所有者缺陷。
4 changes: 3 additions & 1 deletion BUILD_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,9 @@ scripts/gates/run-wasm-smoke-gates.sh Debug
- 设置持久化:通过 UI 切换应用语言并修改外观、数据与存储、快捷键、ACP 和 MCP 服务器状态,验证 shell 重载后的 `x:Uid` 与 singleton ViewModel 文案,并在刷新后从可见设置页确认状态仍存在;
- ACP / 平台能力边界:保存 ACP WebSocket profile,验证 WASM 不声明 `clientCapabilities.fs` / `terminal=true`,并确认受限平台不会暴露桌面文件系统入口;
- Gamepad 能力边界:确认 BrowserWasm 通过浏览器 Gamepad API 投影标准手柄读数,并验证 DPad / A 键进入 Diagnostics 的 pressed 槽位与 ActiveInputs 意图事实(`MoveDown` / `Activate`)。原生焦点由键盘/XYFocus 与控件 consumer 拥有;壳层不再对轮询手柄做全局 SendInput / FocusManager 焦点 bridge,避免双路径分发;
- WASM ACP 全链路:用同一 profile 和 remote directory 从 Start 页面创建远端会话,断言 mock ACP Server 收到 `initialize`、`session/new`(`cwd` 为所选 remote path)和 `session/prompt`,并确认 agent reply 投影到 Chat UI。
- WASM ACP 全链路:用同一 profile 和 remote directory 从 Start 页面创建远端会话,断言 mock ACP Server 收到 `initialize`、`session/new`(`cwd` 为所选 remote path)和 `session/prompt`,并确认 agent reply 投影到 Chat UI;在 `session/new` 完成前验证请求级 elicitation 收到 `cancel`,再通过真实表单填写验证必填校验、字符串与布尔值回包,以及未知必填字段禁止提交且可取消。

设置持久化与 ACP 全链路在刷新前,只读等待本轮配置出现在 IDBFS 的 IndexedDB 已提交记录中,再从重载后的 UI 验证;内存 `FS.readFile`、按钮恢复可用或固定等待均不代表落盘完成,gate 不代应用调用 `syncfs`。

它补充 Windows FlaUI gate,专门覆盖 WASM 浏览器里的原生 Uno 控件行为与当前构建产物的浏览器持久化链路。

Expand Down
5 changes: 5 additions & 0 deletions SalmonEgg/SalmonEgg/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using SalmonEgg.Infrastructure.Services;
using SalmonEgg.Infrastructure.Storage;
using SalmonEgg.Infrastructure.Transport;
using SalmonEgg.Presentation.Core.Localization;
using SalmonEgg.Presentation.Core.Mvux.Chat;
using SalmonEgg.Presentation.Core.Mvux.ShellLayout;
using SalmonEgg.Presentation.Core.Resources;
Expand Down Expand Up @@ -512,6 +513,7 @@ private static void RegisterInfrastructureServices(IServiceCollection services)
? new TerminalSessionManager()
: new UnsupportedTerminalSessionManager());
#endif
services.AddSingleton<ITransportErrorMessageFormatter, TransportErrorMessageFormatter>();
services.AddSingleton<IAcpClientFactory, AcpClientFactory>();

// ACP setup wizard. The catalog is pure data and safe everywhere; every probing, installing and
Expand Down Expand Up @@ -942,6 +944,9 @@ private static void RegisterInfrastructureServices(IServiceCollection services)
services.AddSingleton<ICliCommandRegistrationInspector, UnsupportedCliCommandRegistrationInspector>();
services.AddSingleton<ICliCommandLinkService, UnsupportedCliCommandLinkService>();
#endif
// Intentional exception to coding-standards §3.1: these commands operate on one machine-wide
// registration. Settings page instances share the command owner and IsBusy so their buttons
// observe the same in-flight inspect/link operation instead of maintaining separate busy state.
services.AddSingleton<CommandLineSettingsViewModel>();
services.AddSingleton<McpSettingsViewModel>(sp =>
new McpSettingsViewModel(
Expand Down
29 changes: 21 additions & 8 deletions SalmonEgg/SalmonEgg/Presentation/Views/Chat/ChatView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
AllowDrop="False">

<Page.Resources>
<!-- Form grouping follows the existing chat card rhythm; keep these dimensions in one
resource scope so theme/layout changes do not require editing individual controls. -->
<Thickness x:Key="ChatElicitationMargin">12,0,12,8</Thickness>
<Thickness x:Key="ChatElicitationPadding">16</Thickness>
<Thickness x:Key="ChatElicitationBorderThickness">1</Thickness>
<Thickness x:Key="ChatElicitationFieldMargin">0,0,0,12</Thickness>
<CornerRadius x:Key="ChatElicitationCornerRadius">12</CornerRadius>
<x:Double x:Key="ChatElicitationSectionSpacing">16</x:Double>
<x:Double x:Key="ChatElicitationHeadingSpacing">4</x:Double>
<x:Double x:Key="ChatElicitationFieldSpacing">6</x:Double>
<x:Double x:Key="ChatElicitationActionSpacing">8</x:Double>
</Page.Resources>

<Grid>
Expand Down Expand Up @@ -465,14 +476,14 @@
Visibility="{x:Bind ViewModel.HasPendingElicitationRequest, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<controls:ResponsiveContentHost.Child>
<Border x:Name="ElicitationHost"
Margin="12,0,12,8"
Padding="16"
CornerRadius="12"
Margin="{StaticResource ChatElicitationMargin}"
Padding="{StaticResource ChatElicitationPadding}"
CornerRadius="{StaticResource ChatElicitationCornerRadius}"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="1">
<StackPanel Spacing="16">
<StackPanel Spacing="4">
BorderThickness="{StaticResource ChatElicitationBorderThickness}">
<StackPanel Spacing="{StaticResource ChatElicitationSectionSpacing}">
<StackPanel Spacing="{StaticResource ChatElicitationHeadingSpacing}">
<TextBlock x:Uid="ChatViewElicitationTitle"
FontSize="14"
FontWeight="SemiBold"
Expand All @@ -485,7 +496,8 @@
<ItemsControl ItemsSource="{x:Bind ViewModel.ElicitationFields, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="elicitationVm:ElicitationFieldViewModel">
<StackPanel Spacing="6" Margin="0,0,0,12">
<StackPanel Spacing="{StaticResource ChatElicitationFieldSpacing}"
Margin="{StaticResource ChatElicitationFieldMargin}">
<TextBlock Text="{x:Bind Title}" FontWeight="SemiBold"/>
<TextBlock Text="{x:Bind Description}"
TextWrapping="WrapWholeWords"
Expand Down Expand Up @@ -523,7 +535,8 @@
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
TextWrapping="WrapWholeWords"/>

<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
Spacing="{StaticResource ChatElicitationActionSpacing}">
<Button x:Uid="ChatViewElicitationCancelButton"
Command="{x:Bind ViewModel.ElicitationCancelCommand, Mode=OneWay}"/>
<Button x:Uid="ChatViewElicitationDeclineButton"
Expand Down
Loading
Loading