Commit Graph
373 Commits
Author SHA1 Message Date
胡飞 450a693db8 docs(design): 记录 macOS/Linux 三产品打包进度 2026-10-29 03:07:01 +08:00
胡飞 b11e432817 docs(design): 记录 DriverGate 与数据产品驱动安装 2026-10-29 03:04:40 +08:00
胡飞 406aebe374 docs(design): 记录三产品启动冒烟结果 2026-10-29 02:57:35 +08:00
胡飞 abe0dddd3c docs(design): 记录独立产品路径初始化 2026-10-29 02:55:03 +08:00
胡飞 d598077df3 docs(design): 记录 navop-data 主页 2026-10-29 02:53:17 +08:00
胡飞 f1b5dc4d1d docs(design): 记录 navop-shell 真实主页 2026-10-29 02:42:14 +08:00
胡飞 ac6f696beb docs(design): 记录 main lib 化与 apps/navop 全量包 2026-10-29 02:35:03 +08:00
胡飞 0e6ecd79e0 docs(design): 记录 runtime_redis/runtime_sftp 拆分 2026-10-29 02:29:26 +08:00
胡飞 ca1ce5d90c docs(design): 记录 runtime_core/runtime_database 拆分 2026-10-29 02:26:38 +08:00
胡飞 b7fa5ff7e3 docs(design): 记录会话日志领域迁移 2026-10-29 02:20:26 +08:00
胡飞 d40a5130ca docs(design): 记录 RDP/VNC 领域迁移 2026-10-29 02:14:31 +08:00
胡飞 658dd71540 docs(design): 记录惰性打开修复 2026-10-29 02:07:46 +08:00
胡飞 f0bfaf0e5a docs(design): 记录端口转发领域迁移 2026-10-29 01:50:50 +08:00
胡飞 d127c4cef2 docs(design): 记录 SFTP 领域迁移 2026-10-29 01:47:37 +08:00
胡飞 082eb5f8ec docs(design): 记录 SSH 领域迁移与事件桥 2026-10-29 01:43:10 +08:00
胡飞 78cd7f038a docs(design): 记录本地终端迁移 2026-10-29 01:31:31 +08:00
胡飞 f548b9618f docs(design): 修正 MongoDB 提交号 2026-10-29 01:14:30 +08:00
胡飞 de6ca16d66 docs(design): 记录 MongoDB 迁移 2026-10-29 01:14:28 +08:00
胡飞 fd1d63166b docs(design): 记录 Database 打开路径迁移 2026-10-29 01:12:07 +08:00
胡飞 e19f7005c1 docs(design): 记录 Redis 工作区迁移 2026-10-29 01:07:47 +08:00
胡飞 b099e1936f docs(design): 记录 data_domain Redis 迁移 2026-10-29 01:05:05 +08:00
胡飞 b2b3531d95 docs(design): 记录 Product 化 bins 与 TabOpener 模式参数 2026-10-29 01:00:22 +08:00
胡飞 1ba6df6874 docs(design): 记录 KnownHosts 域迁移进度 2026-10-29 00:57:38 +08:00
胡飞 dbdeb6693e docs(design): 记录 Product 启动契约进度 2026-10-29 00:50:20 +08:00
胡飞 b0a84f775a docs(design): 记录 shell/data 骨架包进度 2026-10-29 00:47:25 +08:00
胡飞 cf60c58b74 docs(design): 更新 app_kit 与身份路径切换进度 2026-10-29 00:45:38 +08:00
胡飞 7f2f5e2e31 docs(design): 记录三产品拆分的实施进度 2026-10-29 00:36:33 +08:00
胡飞 8093ed9d93 docs(design): 增加 navop 三产品彻底拆分设计稿 2026-10-29 00:09:02 +08:00
胡飞 4ec3421523 feat(ai): 终端选中内容随消息送进侧边栏 AI
根因:资源作用域只告诉模型「选中了 N 行」的摘要,正文还得模型自己再调一次
`terminal.read`,用户常常直接在提问里重贴一遍。

修法:
- `ai_chat_view` 新增 `ContextAttachment` 与 provider,发送前把宿主给的正文拼在
  用户输入前面;拼好的文本同时进转录,用户回看就知道自己送出了什么。
- 终端侧边栏接上 provider,把选区包成
  `<terminal_selection source lines truncated>`,超 8000 字截断并标注,
  剩下的仍交给 `terminal.read`。
- `selection` 作用域补一句「正文已随消息附上」,模型不必再猜要不要读。

验证:
- `RUSTFLAGS="-D warnings" cargo test -p terminal_view -p ai_chat_view`:943/12/617/
  5/11 全通过(含 4 条新投影测试、2 条附件拼接测试)。
- `cargo clippy -p terminal_view -p ai_chat_view --all-targets`:仅既有告警。
2026-10-27 10:42:31 +08:00
胡飞 306eb8ce40 feat(ai): 读终端可省略 target,并返回全屏/选中现场
侧边栏 AI 要判断「用户是不是在 vim 里」,原先只能先 connections.list_sessions
筛出终端 id,再 terminal.read 显式传 target;读回来的结果也分不清全屏程序界面和
shell 输出。

- public_mcp 的 agent 适配器把 terminal.read 的 target 从 required 摘掉,模型不传时
  用会话里唯一的终端资源补上;同类资源不止一个就报错让它显式传 id,不猜。MCP 直连
  表面的 schema 不变,仍强制 target。
- agent_runtime 新增 ResourceContext::default_target_of_kind:当前资源类型匹配时优先,
  否则只在同类唯一时返回。
- terminal.read 结果增补 alt_screen / display_offset / cursor_row / cursor_column /
  selection,都在终端锁内一次取全。

验证:
- cargo test -p terminal -p public_mcp -p agent_runtime -p terminal_view -p ai_chat_view
  (RUSTFLAGS=-D warnings)全绿,含新增 2 条 recent_text 现场测试与 4 条 target 可省略测试。
- cargo clippy:改动文件无新增告警。
2026-10-27 09:49:43 +08:00
胡飞 d0e8fe6bee fix(ai): SSH 终端的实时状态也交给侧边栏 AI
上一版刷新器把手上的终端设为默认目标,对 SSH 侧边栏并不成立:那里的默认
目标是连接,sftp.*、ssh.exec 等连接级工具都依赖它,改成终端会让这些工具
全部需要显式 target,属于回归。

改为按侧边栏类型区分默认目标:

- 本地终端:终端仍是默认目标,连接只作为候选。
- SSH 终端:连接保持默认目标,活动终端带实时作用域(cwd / screen /
  selection / size / process)紧跟其后;连接上写 `terminal=<终端 id>`,
  终端上写 `connection=<连接 id>`,两边互相指得到,模型不必先
  `connections.list_sessions` 再筛 session。
- 连接被「禁止 AI 操作」时返回 None,沿用宿主静态资源池,避免绕过连接级策略。

接线:TerminalSidebar 在连接模式下把默认目标的资源 id 记在
`ai_connection_resource`,initialization 建完侧边栏后交给刷新器;刷新器每轮
重读连接列表,连接的改名/删除/策略变更下一轮即生效。

验证:
- cargo test -p terminal_view -p ai_chat_view(RUSTFLAGS=-D warnings):
  941 + 12 + 613 等全部通过,新增 SSH 语义用例 3 条。
- cargo clippy -p terminal_view --all-targets:新增代码无告警。
- 设计文档补 2.3 默认目标对比与 2.4 SSH 接线。
2026-10-27 09:24:33 +08:00
胡飞 620f74584a feat(ai): 侧边栏 AI 每轮感知终端实时状态
终端侧边栏的 AI 资源池原先只在面板创建时构建一次,之后永久冻结:用户
cd 到别的目录、在 vim 里编辑、选中一段报错后,模型都不知情,必须主动
调用 terminal.read 才能看到现场,而它往往不会调用。

- ai_chat_view 新增 ResourceContextRefresher 回调:发送前在主线程重建
  资源池并写回 session,AgentTask 每步重渲染 system prompt 时自然带上
  最新状态;DefaultAgentChatPanel 支持宿主在视图建好前后注入。
- terminal_view 新增 ai_live_context 模块,把终端的当前目录、是否运行
  全屏程序、选中摘要、尺寸、进程状态投影成资源作用域,并让当前终端始终
  保持默认目标(连接等资源只作为 target 候选)。
- 新增 docs/design/agent-live-resource-context.md 记录设计与取舍。

验证:cargo test -p terminal_view -p ai_chat_view 全绿(含 ai_live_context
9 条新增单测);cargo clippy -p terminal_view -p ai_chat_view --all-targets
新增代码无告警。
2026-10-26 19:01:24 +08:00
胡飞 aa8a1558a3 Merge branch 'dev' into impl/ai-workbench
把 origin/dev(15e41c0f7,62 个提交)合进本分支,带进:
- 快捷打开一次建两个 SSH 会话的修复(#461/#462)
- 个人同步导出凭据引用归一化、git 同步冲突「应用」修复
- 跨块拖选覆盖表格/分隔线的高亮、PostgreSQL 字符列默认值补引号
- 连接级 AI 访问策略(migration 20261022000001_connection_ai_access)
- v0.21.1 发布与 changelog 更新

冲突只三处,解法:
- AGENTS.md:两侧都是纯新增,取并集(本分支 6 条经验 + dev 的 GUI spawn npx 一条)
- crates/ai_chat_view/locales/ai_chat_view.yml:保留 search_branch / search_model,
  dev 的 search_models 随其手写模型搜索框一并弃用
- crates/ai_chat_view/src/input/agent_input.rs:模型下拉保留本分支的组件库 Select
  (自带搜索框、虚拟滚动、键盘导航,是 dev 手写 Popover+搜索框的超集),删掉
  dev 侧的 model_search_input/model_search_needs_reset、MODEL_MENU_* 常量、
  filter_model_options/model_option_matches 及其单测
2026-10-25 23:04:33 +08:00
胡飞 0c7b757604 feat(connection): 连接级 AI 访问策略(issue #437)
问题:AI 可能绕过只读工具,直接登录「只打算人工管理」的连接执行写命令。

方案:连接新增 `ai_access_level`(允许 / 只读 / 禁止),三层落地:
- 可见性:资源池与内置 AI 的资源目录、@ 提及按级别过滤或裁剪能力;
  `connections.list/find` 不返回被禁止的连接;
- 路由:目标解析在能力不匹配时直接失败,模型拿不到句柄;
- 执行:`ToolRegistry` 挂 `ToolAccessGuard`,外部 MCP 与内置 AI 共用同一闸门,
  覆盖 `connection` / `connection_id` / `target` / `session_id` 四类目标。

策略属于连接内容:随云同步与导入导出走(载荷新增 `ai_access_level`,并加入团队
冲突判定的内容变更白名单),保证换机器、换客户端后「生产就是生产」的限制仍生效。

界面:连接表单(数据库 / SSH / Redis / MongoDB / middleware)三选一,侧栏徽标与
右键菜单可直接切换;写失败显式报错,不静默放宽。

Refs #437
2026-10-22 11:25:32 +08:00
keenser 8b4911b953 feat(connections): add custom icon support for extensions and SSH
Implement icon picker for connection forms, allowing users to select
built-in OS icons or upload local images. Add Icon field type to
declarative forms and extension manifests. Centralize icon resolution
logic in core storage and universal plugins to ensure consistent
display across connection lists, cards, and terminal tabs.
2026-10-21 11:38:00 +03:00
keenserandClaude Sonnet 5 aee8d21767 feat(extensions): add provider-less terminal connections
A connection contribution can now declare `terminal` (command, args, env, title) instead of a provider runtime. The host resolves the templates against the saved non-secret form values and opens a regular local terminal tab, so extensions like tsh-ssh need nothing but an extension.json.

- Manifest: runtimeId/resourceType become optional; `terminal` is mutually exclusive with runtimeId, resourceType, shellViewId and workbench bindings. The command is fixed (no placeholders) and placeholders may only reference existing non-secret fields.
- Host-side guard: an argument whose template does not start with '-' is rejected if the substituted value does, so shared or synced connection data cannot inject command-line options. Empty optional args/env entries are dropped, control characters are rejected.
- Terminal: launched sessions get a fixed tab title, can be duplicated, and restart on Enter after the child process exits.
- Connection form hides the Test button for terminal connections.
- Docs: describe terminal connections in the extension development guide.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-17 14:55:17 +03:00
胡飞 d1faa844e1 Merge remote-tracking branch 'origin/dev' into impl/ai-workbench
dev 侧 70 个提交进来。主要是把后端从 gpui-pre 换成 gpui-fast(开启了保留渲染
view retention),另外还有 SQL 库、WebDAV 个人同步的跨实例互斥(远端锁文件)、
终端 reintegrate、一堆 db_view / shell / 打包修复。

两处冲突,都按「谁有实质改动就给谁」处理:

- `crates/ai_chat_view/src/acp/connection/prompt.rs`:dev 那边只是 rustfmt 重排,
  语义改动(cancel 握手期间保住回包 future,别拆掉 JSON-RPC 响应路由的接收端)
  我这边已经用「无进展判据」重写过(`wait_for_prompt` + `PromptWait`,比按总时长
  超时更准),取我这边。
- `crates/core/src/cloud_sync/personal/webdav_store.rs`:dev 新增了远端锁文件
  (`WebDavLockFile` / `load_lock_file`,真正的跨实例互斥),我这边只是 rustfmt
  重排,取 dev 那版。

其余文件自动合并。合并后 `cargo check --workspace --all-targets` 通过;`ai_chat_view`
测试在保留渲染下暴露出两处「账面残留」(元素已不再绘制,调试选择器却还挂在
`debug_bounds` 里),另起一笔修,不混进合并。
2026-10-12 17:41:11 +08:00
泡芙 70ac3947c1 docs: 补齐 gpui-pre 的 deferred 判据改动(C),并核实 gpui-kit 无需改动
- §1 / §3 补上改动 C:`Window::deferred_content_present` 与
  `Window::has_deferred_content()`。让位判据必须由 GPUI 提供 —— 决定屏幕层级的是
  overlay surface,而它的内容来源就是 gpui 的 deferred 层,Window 之外拿不到;
  并写明两条语义:描述「上一帧」(窗口 idle 时保持旧值),且只镜像 `deferred_draws`
  (普通元素树内容不计入)
- diff 汇总更新为 A / B / C 三处(+58/−9),补丁路径换成本轮那份
  `.workbuddy/tmp/rdp-cloak-fix/gpui-pre-fork.patch`,并注明它已落地在
  `tag = "fork-0.3.126"` 指向的 rev 上
- 新增 §10:gpui-kit 无需改动。41 处 `deferred(` 已覆盖 dialog / popup / tooltip /
  context menu / popup menu / app menu bar / select / date picker / plot tooltip /
  resize handle / dock panel / shell root,三个 checkout 工作区均干净

验证:
- `grep -c "has_deferred_content"` 在 `fork-0.3.126` 对应的 rev 上为 1,
  其余 5 个 checkout 为 0 —— 即该 tag 已含改动 C
- 文档结论与当前依赖状态一致
2026-10-10 22:11:03 +08:00
泡芙 b920b53626 docs: 记录原生 RDP airspace 的合成呈现方案、按需 cloak 与真机判据
- `gpui-pre-airspace-surface-forwarding.md`:gpui-pre fork 侧的两处必要补丁 ——
  `ManagedPlatformSurface` 漏转发 `set_window_content`(调用落到 trait 默认实现
  直接 bail),以及 `DirectCompositionPortal` 用 `IDCompositionVisual3` 控制可见性
  而合成设备只到 v1(`cast` 必然 `E_NOINTERFACE` / `0x80004002`)
- `windows-native-rdp-cloak-ordering.md`:症状与唯一可信判据(必须取像素)、
  合成路径与按需 cloak 的时序约束、§9~§11 的逐轮修正,以及「真机实测对照表」;
  含一个反直觉结论:`DWMWA_CLOAK` 落得太早会冻结空的合成 surface,此时所有
  DComp 调用仍返回 S_OK,只能靠像素判据发现

验证:
- 文档中的判据脚本均在真机执行过,结论与代码当前状态一致
2026-10-10 22:08:24 +08:00
胡飞 1542a5eb5f feat(ai_chat_view): steer 插话 + 标题/用量落盘 + 重连回归
P0 steer:连接层 register_turn 不再对同会话在飞轮返回 AlreadyRunning,
直接并发入表(相位仍属首个轮次);notifications 喂同 protocol_session_id
全部 tracker,事件归 registered_at 最新一轮;claim_prompt_completion
归别人时 Reclaimed 收表不动相位。视图 submit() 新增 steer 分支:ACP +
前台轮在飞 + 队列空时直接插话(acp_steer_sent 提示),RetryLater 退回
排队;队列非空保 FIFO 不插队。实测 claude-agent-acp 0.88.0 / codex-acp
2.1.1 握手均声明 _meta.steering.supported。

P1 会话标题:persist_acp_session 标题优先级改为 agent 推送
(SessionInfoUpdate) → 本地转录首条用户消息 → agent 名;落盘后同步
upsert_live_summary,侧栏立即见新名,不等重启。

P1 用量落盘:persist_acp_session 把 AcpUsage.used 写进 runtime session
context_tokens,随快照持久化(复用现有通道,不为趋势图先建表)。

P1 断线重连:补决策回归测试——在飞轮次(owner 非空)让路不重连,
轮次收尾后恢复。resume/load/降级新建提示链路经审查已完整。

P0 性能纪律:新增 docs/design/ai-workbench-streaming-performance.md,
固化事件批处理预算、broadcast 4096 容量、Lagged 单次自愈、摘要行
不进大快照等红线与压测方法。

测试:842 通过(+steer +重连决策)。
2026-10-09 14:54:30 +08:00
胡飞 a246ef8bfd macos: 关掉「关闭即隐藏」开关,窗口重新走 remove_window()
上游 zed#65186(gpui_macos: Fix crash closing windows on Touch Bar Macs)把根因
修掉了:accesskit 不再用 SubclassingAdapter 动态替换内容视图的类,改成挂在
GPUIView 上的普通 Adapter。那次「还原类」正是破坏 AppKit Touch Bar 观察者 KVO
状态、让窗口关闭时抛异常的动作。该提交随 zed upstream/main 合进 fork,发布为
gpui-pre fork-0.3.124。

因此:
- release.yml 不再给 x86_64-apple-darwin 传 macos-touchbar-window-hide,
  extra_features 变量与其分支一并删除,三个平台交付同一形态:关闭即销毁。
- script/test-release-packaging.mjs 同步改成断言这一步不再出现 --features /
  extra_features,29 项全过。
- 机制整套保留(HIDE_WINDOWS_ON_CLOSE 常量、隐藏路径、关闭漏斗三态、弹窗登记表
  与全部契约测试),现在恒为 false;重新打开只需给对应 target 传该 feature。
  已验证 --features macos-touchbar-window-hide 下 one-core 仍以 -D warnings
  干净编译,回退路径可用。

改动只涉及构建开关、注释与文档,Rust 逻辑未动:one-core 关窗/弹窗契约测试
(8 + 14)、remote_file_editor 契约测试 6 项、main 的 -D warnings 编译均通过。
本次没有真机验证 —— 手上没有 Touch Bar 机型;判定依据是上游把出问题的那条
代码路径整个删掉了。若闪退复现,第一动作是重新打开这个开关。
2026-10-06 17:16:42 +08:00
胡飞 b2fb4e2489 fix(ui): 保存流程不再依赖「窗口会消失」,关闭失败可重试
隐藏失败(`WindowCloseOutcome::Retained`)会把保存窗口留在屏幕上,而旧保存流程是靠
「窗口反正会消失」结束这一轮的:表单没有切到「已保存」,用户再点一次「保存」会再插
一条连接(redis / mongo / serial / port_forwarding 最明显,异步任务成功前窗口已经关过
一次),db / 中间件 / 扩展 / 凭据表单则是把 `editing_connection` 清成 None 后重建。

- `close_plan(is_popup, &hide)`:关窗决策抽成纯函数,四种输入组合(含注入的隐藏失败)
  由单测钉住;`WindowCloseOutcome` 加 `#[must_use]`,忽略要显式 `let _ =`
- 新增 `close_window_after_save`:只在 `Retained` 时提示「已保存,但窗口没能关闭,
  可以重试关闭」,裸关窗(取消 / 红点 / Cmd-W)不受影响
- 12 个保存类入口在成功后**就地**切「已保存」(`mark_saved` / 写回 `editing_connection`),
  下一次保存走更新而不是新建
- 异步保存表单(spawn 后立刻关窗)在保存落地后用 `cx.update_window` 再补一次
  「切状态 + 关窗」,提示因此能带上「已保存」这个事实
- 契约测试:`save_flows_close_through_the_save_aware_funnel`、
  `save_flows_flip_the_form_into_its_saved_state`
- 验收文档补 §5.1 资源预算(`C_shell` / S2 增量 / 停放壳峰值)与「超预算即暂停扩大
  交付」的停止条件,并把 §1 里「已知且接受」改成「受预算约束」
2026-09-29 14:25:10 +08:00
胡飞 83fb5ea692 docs: 修正 v0.19.4 范围表述,补窗口复用内部验收方案
CHANGELOG v0.19.4 中英文条目:把「Touch Bar 只存在于 x86_64」改成「当前只在 Intel 包里默认打开」,并点明 Apple Silicon 的 13 英寸 MacBook Pro 也有 Touch Bar。§10.12 同步。

新增 docs/macos-window-reuse-acceptance.md:计数怎么读、6 个实机场景、Instruments 代际验证、通过标准与停止条件,以及明确划出去的「空闲壳池」与「守卫异常追踪」。
2026-09-29 10:16:15 +08:00
胡飞 6635cd4b4c refactor(window): 弹窗一律复用、视图内关闭一律走关闭漏斗,补齐「所有打开窗口的地方」
开关只管「关闭时隐藏」,但如果还有别的入口自己 `window.remove_window()`,
那条入口照样会销毁原生窗口 —— #308 的「确定 / 取消」、#314 的「保存」就是这么漏出来的。
这一版把「所有打开窗口的地方」都收到同一套机制下。

- 24 处一次性弹窗改成 `open_reusable_popup_window`,并给每个目标一个复用键
  (`connection-form:ssh:42`、`table-export:{conn}:{db}.{schema}.{table}`、
  `remote-desktop:{destination}`、`credential-form:{id}` 等),
  为此给各表单窗口的 config 结构补上 `Clone`:复用时要按本轮目标重建 view。
- 15 个表单 / 工具栏窗口里视图自己发起的关闭改成
  `one_core::window_close::close_window_for_reuse(window, cx)`。
- 契约测试 `secondary_windows_never_destroy_themselves` 逐文件断言这些文件里不再出现
  `window.remove_window()`:漏掉任何一个入口,那条入口就照旧销毁原生窗口。
- 文档 §10.12 记录开关的生效范围与这次收敛(含「ARM Mac 不开」的理由:
  没有 Touch Bar 就没有崩溃链,代价却是真实的)。

验证:`cargo check -p main`(feature 关)exit 0;`cargo check -p main --features
macos-touchbar-window-hide`(Intel Mac 交付形态)exit 0;`cargo test -p one-core --lib`
两种配置各 698 passed(含新契约测试);`cargo test -p remote_file_editor --lib` 70 passed;
`cargo clippy -p one-core --features macos-touchbar-window-hide --all-targets` 改动文件零告警。
2026-09-28 20:58:05 +08:00
胡飞 5a0959f0d1 chore(deps): 切到 gpui-pre fork-0.3.122(守卫的 @try/@catch 移出 Rust 帧,release 的 panic=abort 下才有效)
`fork-0.3.121` 的守卫单测全绿,却在 navop 自己的 release 配置下是坏的:它的
`@try`/`@catch` 用的是 `objc2::exception::catch`,把 Rust 闭包包在 `@try` 里;而 navop 的
`[profile.release]` 是 `panic = "abort"`(省掉 `__eh_frame` / `__gcc_except_tab`),这种构建
下 ObjC 的 unwind 穿过 Rust 帧会变成 `panic in a function that cannot unwind` 并直接 abort。
cargo 对 test/bench profile 忽略 `panic` 设置,所以 16 个单测全过说明不了 release 包的问题。
独立探针(与 navop 一致的 release profile + `#[path]` 引入真实守卫源码)复现:重复注销这一步
在 dev 下被吸收,换成 release 配置后退出码 **134**,`@catch` 根本没执行。

`fork-0.3.122` 把 `@try`/`@catch` 挪进 crate 自己编译的 Objective-C
(`crates/gpui_macos/objc/gpui_macos_try_remove.m`,由 `build.rs` 用 `cc` 编译):`@try` 体里
**直接调用原实现**,raise 与 `@catch` 之间没有 Rust 帧,因此与构建的 panic 策略无关。shim 返回
被 retain 的异常,Rust 侧照旧判断:查找器的重复注销(`nextResponder` + `NSRangeException` +
`because it is not registered as an observer`)吸收并记 warn,其余交给 shim 的 `@throw` 原样
重抛(仍以 `C-unwind` 穿过被替换的实现)。顺带把 `objc2` 的 `exception` feature 关掉 —— 树里
已经没人用它,留着只会把同一个 abort 请回来。

## 验证

- 独立探针 + navop 的 release profile:吸收生效并打出完整 warn,通知计数 before=1 / after=1
  (首次注销真的删掉了,通知不再来),进程正常退出。
- 同一探针换回 `fork-0.3.121` 的守卫做对照:`panic in a function that cannot unwind` →
  `thread caused non-unwinding panic. aborting.` → 退出码 **134** —— 探针确实能检出这个问题。
- 上游 `cargo test -p gpui_macos --lib`:16 passed / 0 failed。
- 快照 `4188a6b2`(`zed-rev` = `283416671d`)已确认带上 `crates/gpui_macos/build.rs`、
  `objc/gpui_macos_try_remove.m` 与 `cc` build-dependency(少任何一个都会在链接期失败)。
- 本仓 24 条 `[patch.crates-io]` 与 `Cargo.lock` 同步切换;`cargo check -p one-core -p main
  --all-targets` 通过。

docs/macos-memory-investigation.md 新增 §10.10(改成「只吸收那一个异常」,附「单测绿≠release
可用」的教训)与 §10.11(shim 修法与全部验证记录)。

真机包组合 = 弹窗只隐藏不销毁(§10.7)+ `fork-0.3.122` 的守卫。
2026-09-28 17:44:03 +08:00
胡飞 99fc35bd58 chore(deps): 切到 gpui-pre fork-0.3.121(守卫只吸收那一个重复注销异常)
fork-0.3.121 是 Touch Bar 守卫的第三版,方向从「跳过」改成「只吸收那一个异常」。

## 为什么还要再改

fork-0.3.118 的守卫对查找器发起的注销一律直接 return。这太重:KVO 的契约要求合法注册必须
可注销,跳过第一次合法注销会留下「已注册、但调用方以为已注销」的观察项 —— 真机退出应用时
崩在 Foundation KVO 记账里(`navop-2026-09-27-165228.ips`,`KERN_INVALID_ADDRESS`)的头号
嫌疑就是它。

## fork-0.3.121 做了什么

- 每一次注销都真的执行,转发放进 `objc2::exception::catch`(`objc2` 的 `exception` feature,
  即 C 编译出来的 `@try`/`@catch`)。正常返回 ⇒ 注销真的发生了,记账保持一致。
- 抛出时只匹配这一种组合:观察者是查找器的、key path 是 `nextResponder`、异常名
  `NSRangeException`、reason 含 `because it is not registered as an observer`。匹配 ⇒ 记一条
  warn(观察者类与地址、被观察对象类与地址、key path、context、异常名与 reason)后吸收。
- 不匹配 ⇒ `objc2::exception::throw` 原样重抛,让它像从被替换的实现里抛出一样 `C-unwind`
  穿过守卫。Cocoa 不是异常安全的,网撒太大会把 bug 藏起来而不是修掉。

`fork-0.3.121` 快照提交 `c335307a`,`zed-rev` = `83e4b04b60`
(`gpui_macos: Answer the Touch Bar finder's duplicate retraction instead of skipping retractions`)。

## 验证

- 上游 `cargo test -p gpui_macos --lib`:16 passed / 0 failed。新增
  `the_finders_first_retraction_removes_and_the_duplicate_is_absorbed`(旧版做错的那件事)、
  `another_key_paths_retraction_is_thrown_back` 与
  `only_the_unregistered_next_responder_retraction_counts`(把网收窄)、以及 `rethrow` probe。
- 本仓 `cargo check -p one-core -p main --all-targets`:通过(exit 0)。
- 24 条 `[patch.crates-io]` 与 `Cargo.lock` 同步指向 `fork-0.3.121`。

docs/macos-memory-investigation.md 新增 §10.10 记录这一轮的判断与证据。

真机第五版组合 = 弹窗只隐藏不销毁(§10.7)+ 这版守卫。
2026-09-28 16:07:41 +08:00
胡飞 e9bed23840 chore(deps): 切到 gpui-pre fork-0.3.120(Touch Bar 守卫恢复默认开启)
真机(Intel + Touch Bar,上一版 `0.19.2-touchbar-hide-only-noguard`)反馈:红点关闭与
RDP 全屏不再闪退,但点弹窗的「确定 / 取消」仍然闪退。交回的
`navop-2026-09-28-123639.ips`(`slice_uuid` 正是该包的 `F6C3E655-…`)栈与最早那份同型:

    -[NSApplication _crashOnException:]
      ← __exceptionPreprocess ← objc_exception_throw
      ← -[NSObject _removeObserver:forProperty:]
      ← removeObserver:forKeyPath: ← removeObserver:forKeyPath:context:
      ← -[_NSTouchBarFinderObservation invalidate]
      ← ___NSTouchBarFinderSetNeedsUpdateOnMain_block_invoke_2
      ← NSDisplayCycleObserverInvoke ← NSDisplayCycleFlush ← CATransaction ← 主 runloop

链里没有一帧与「窗口被销毁」有关:被注销的 observation 属于一个还活着的视图。也就是说
§10.8 里「不销毁窗口就不会再触发守卫要挡的场景」这个假设被真机否掉了 —— 同一轮显示周期里
两次 `nextResponder` 链变化就足以触发。

`fork-0.3.120` 把开关语义反过来:**默认安装**,只有 `GPUI_MACOS_TOUCHBAR_GUARD` 显式写成
`0`/`false`/`off`/`no` 才不装,空串按「没设」处理。守卫自身的递归缺陷已在 0.3.119 修掉
(每类各自的转发器),两者不冲突。

验证:`cargo test -p gpui_macos --lib` 14 passed / 0 failed,其中
`the_guard_is_on_unless_the_environment_says_off` 同时断言「什么都不设 ⇒ 守卫确实装上」与
「设成 `0` ⇒ 确实不装」;`cargo check -p one-core -p main --all-targets` 通过。
文档见 docs/macos-memory-investigation.md §10.9。

仍未解释:`navop-2026-09-27-165228.ips`(退出应用时 Foundation KVO 记账里
`KERN_INVALID_ADDRESS`),只在带着守卫时出现过,已写进文档,等真机复现再定位。
2026-09-28 13:25:58 +08:00
胡飞 c17ba12967 chore(deps): 切到 gpui-pre fork-0.3.119(守卫修好递归,并默认不再安装)
同一台 Intel + Touch Bar 机器交回两份 `.ips`,都是装了上一版守卫
(`0.19.2-touchbar-kvo-noop`)的构建,而且都不是守卫原本想挡的那个异常:

- `navop-2026-09-28-083919.ips`:512 帧互相调用的
  `removeObserver:forKeyPath:context:`,撞到栈保护页后 abort。
  守卫把同一个替换 IMP 装在 `NSObject` 和每个自己实现该方法的
  `NSResponder` 子类上,转发目标却按接收者类现场查找;
  `-[NSWindow removeObserver:forKeyPath:context:]` 自己也实现了这个方法并会
  `[super …]` 交给 `NSObject`,而那份实现同样被替换了 —— 从窗口进入时它就
  调用自己的上游、再被上游调回来。守卫自己成了崩溃源。
- `navop-2026-09-27-165228.ips`:`SIGSEGV` 落在
  `-[NSApplication terminate:]` 里的 Foundation KVO 记账中(收到垃圾 isa)。
  推断(未证实):「凡查找器的注销一律跳过」把 observation 留在了被观察对象
  上,而 KVO 不持有 observer,退出流程遍历时踩到悬空项。

`fork-0.3.119` 的 `touch_bar_guard` 因此改了两点:每个类装属于自己的转发器
(转发目标不再有歧义,probe 新增「窗口发起的注销必须以异常的 SIGABRT 结束,
而不是栈溢出的 SIGSEGV」这条),并且默认不安装 —— 只有
`GPUI_MACOS_TOUCHBAR_GUARD` 打开时才装。弹窗侧已经改成一律不销毁(#315),
守卫要挡的「视图将死时被注销」不复现,那就不必把未证实的退出期风险一起带上。

验证:`cargo test -p gpui_macos --lib` 14 passed / 0 failed;
`cargo check -p one-core -p main --all-targets` 通过;
navop 的 `[patch.crates-io]` 24 条与 `Cargo.lock` 一起切到快照
`36429a19`(`zed-rev` = `b73d4c2b25`)。详见
docs/macos-memory-investigation.md §10.8。
2026-09-28 09:38:22 +08:00
胡飞 5c19d51a2b fix(one-core): 弹窗关闭一律只隐藏、不销毁,红点这条路也不再交给 AppKit
真机反馈:0.19.2-touchbar-kvo-noop 上「确定」不再闪退,但点原生红点仍然闪退。
也就是说变量不是「有没有守卫」,而是**谁发起的销毁** —— 我们自己那条
(`remove_window()` + GPUI 延迟回收)已经安全,AppKit 在自己的关闭流程里那条还是会崩。

既然差别就在「谁销毁」,那就把「销毁弹窗」这件事整个拿掉:

- `popup_window.rs`:新增 `PARKED_POPUPS`(窗口 id → 内容实体),没有复用键的一次性弹窗
  也登记;`reusable_popup_content` 收敛为 `popup_content`(复用登记 + 停放两处查询),
  `is_reusable_popup` → `is_popup_window`,`end_reusable_popup_session` → `end_popup_session`,
  `forget_reusable_popup_by_window` → `forget_popup_by_window`(两类登记都按 window_id 清理)。
- `window_close.rs`:`close_window_for_reuse` 对弹窗只隐藏(`orderOut`)+ 结束业务会话,
  不再 `remove_window()`;销毁只剩「不是弹窗」(主窗口、远程编辑器窗口等不经过弹窗登记表的
  窗口)与两个隐藏失败分支。
- 红点路线保持上一版的接法:先返回 `false` 取消 AppKit 的关闭,再走同一个漏斗。

代价(写在明处):一次性弹窗没有复用键,隐藏后不会被重新显示,下一次打开是新建窗口 ⇒
停放窗口数随打开次数增长,直到退出应用。每个停放窗口只是空壳(业务 view 在关闭时已卸载)。
收敛方向是把热点弹窗改成 `open_reusable_popup_window`(数量收敛到弹窗种类数,且不需要任何
销毁),而不是加回销毁;在还有窗口必须销毁之前,不考虑「停放上限 + 延迟销毁」。

验证:

- `cargo test -p one-core --lib` → **693 passed / 0 failed**
- `cargo check -p one-core -p main --all-targets` → `Finished`(exit 0)
- `rustfmt --edition 2024` 只跑改动的两个文件
- 契约测试:`popup_windows_are_hidden_and_never_destroyed`(弹窗判断在隐藏之前;成功隐藏的
  分支里绝不允许出现 `remove_window()`)、`one_shot_popups_are_registered_as_parked`
  (两种登记都在,且一次性登记必须在 `reuse_key` 的 `else` 分支)、
  `every_popup_intercepts_the_native_close_route` 继续守住关闭路线。
- 文档:`docs/macos-memory-investigation.md` §10.7(并在 §10.6 标注它只对了一半)+
  `popup_lifecycle.rs` 计数口径 + `AGENTS.md` 弹窗条目。

真机判据(同一台 Intel + Touch Bar 机器):点红点关弹窗既不闪退也不卡死,且窗口确实消失。

Refs: navop#308 navop#314, zed#64819
2026-09-27 23:33:17 +08:00
胡飞 926a040cce fix(one-core): 一次性弹窗也接住原生红点,别让 AppKit 在自己的关闭流程里销毁窗口
## 机制

`open_popup_window_inner` 原来只给登记了复用键的弹窗装关闭路线,一次性弹窗(新建 /
保存连接那类)的原生红点仍然走 AppKit 自己的 `windowShouldClose:` →
`-[NSWindow close]` —— 窗口是在 **AppKit 的关闭流程里**被销毁的。那条路上窗口的
响应者(字段编辑器等)可能已经被 AppKit 释放,而 Touch Bar 查找器
(`_NSTouchBarFinder`)的观察要等下一个显示周期(`NSDisplayCycleFlush`)才注销:
`-[_NSTouchBarFinderObservation invalidate]` 于是对一个已经不在的对象调用
`removeObserver:forKeyPath:context:`,异常没人接住,AppKit 的
`-[NSApplication _crashOnException:]` 直接 abort(SIGILL)。gpui fork 里的守卫
(`touch_bar_guard.rs`,fork-0.3.118)只覆盖了**我们自己发起**的那条关闭路线。

现场证据:navop#308(创建 SSH 连接点「确定」闪退)、navop#314(保存连接闪退,
0.18.5 / 0.18.6 上必现,当天 11 份 `.ips`,堆栈与 #308 完全一致)。上游记录 zed#64819。
真机反馈也印证了这条界线:装上守卫之后「点确定不闪退了,点左上角的 x 闪退」。

## 改法

`install_reusable_popup_close_routes` 收敛成 `install_popup_close_routes`,并在
`reuse_key` 分支**之外**调用:

- 红点一律先返回 `false` 取消 AppKit 的关闭,再交给 `close_window_for_reuse`
  决定「隐藏留着复用」还是「交给 GPUI 的延迟销毁」;
- Cmd-W 一类仍走 `set_window_close_handler` + `defer`。

一次性弹窗的语义没变(照样销毁),变的只是**销毁时机**:从 AppKit 的关闭流程里挪回
我们自己的一轮 —— 也就是「保存 / 确定 / 取消」一直在走、现场已经不崩的那条路线。

## 刻意没做

- 没改 `close_window_for_reuse` 的语义(未登记复用键的窗口照旧销毁)。
- 没动 gpui fork:这次是 navop 侧的关闭路线统一,不需要重新发 gpui 版本。
- 没给未登记复用键的窗口补 `orderOut`:销毁就发生在下一轮,不需要先隐藏。

## 验证

- `cargo check -p one-core -p main --all-targets`:`Finished`,无 error。
- `cargo test -p one-core --lib`:**692 passed / 0 failed**。
- 新增契约测试 `every_popup_intercepts_the_native_close_route`:锁住「关闭路线对两类弹窗
  都安装,且这条路线在 `reuse_key` 分支之外」。本机没有 Touch Bar,行为层面无法复现,
  结构断言是唯一能自动化的防线。
- rustfmt:只格式化本次改动的文件。
- ⚠️ 这是假设驱动的改法,真机结论待回:判据是同一台 Intel Touch Bar 机上「点红点关弹窗」
  不再闪退。若仍崩,需要那份 `.ips` 的 `asi` 原文(异常名 / reason / 被观察对象的类名)。

## 旁路发现

应用内更新检查目前拿到 `403 Forbidden (feigeCode/navop)`(GitHub Release 接口匿名限流,
本机日志多次复现),#314 的机器因此停在 0.18.6 并以为「0.18.6 已是最新」。
2026-09-27 23:21:17 +08:00