Skip to content

Cli agent block fix#286

Open
Ghostknight0 wants to merge 9 commits into
zerx-lab:mainfrom
Ghostknight0:cli-agent-block-fix
Open

Cli agent block fix#286
Ghostknight0 wants to merge 9 commits into
zerx-lab:mainfrom
Ghostknight0:cli-agent-block-fix

Conversation

@Ghostknight0

Copy link
Copy Markdown

Description

ssh连跳板机模式下agent cli浮窗功能增强,现在可以随意拖放大小,防止显示不完整;同时可以看到历史对话记录,之前会因为下一轮对话覆盖前一轮的显示。

Testing

已经打包做过测试,没有问题了,可以直接合并~

Server API dependencies

  • Is this change necessary to make the client compatible with a desired server API breaking change?
  • Does this change rely on a new server API?
    • If so, is the use of this API restricted to client channels that rely on the staging server (e.g. WarpDev)?
  • Is this change enabling the use of a server API on client channels that rely on the production server (e.g. WarpStable)?
    • If so, has the new server API been stable on production for at least one server release cycle? See here for more details.

Agent Mode

  • Zap Agent Mode - This PR was created via Zap's AI Agent Mode

Changelog Entries for Stable

CHANGELOG-NEW-FEATURE: {{text goes here...}}
CHANGELOG-IMPROVEMENT: {{text goes here...}}
CHANGELOG-BUG-FIX: {{text goes here...}}
CHANGELOG-BUG-FIX: {{more text goes here...}}
CHANGELOG-IMAGE: {{GCP-hosted URL goes here...}}
CHANGELOG-OZ: {{text goes here...}}

@Ghostknight0

Copy link
Copy Markdown
Author

这个功能对于企业开发很有需要,大部分工作在跳板机完成,望审核后合并~

@zerx-lab zerx-lab left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(详见 inline comments 和下方总结 comment)

— 由 Claude Routine 自动生成;如需复评请 @ 维护者人工触发。


Generated by Claude Code

) -> Vector2F {
// 参考 Warp 的外层约束形态:由 block list 先给浮窗足够大的布局上限,
// 再交给 CLISubagentView 内部 Resizable 处理最终拖拽尺寸。
let max_width = (available_size.x() * CLI_SUBAGENT_MAX_WIDTH_RATIO

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug(边界条件):极窄窗口下外层布局约束上限可能小于内层 Resizable 的 min,产生冲突

cli_subagent_layout_max_size 中:

let max_width = (available_size.x() * CLI_SUBAGENT_MAX_WIDTH_RATIO - CLI_SUBAGENT_HORIZONTAL_MARGIN).max(0.);

对宽度约 375px 的窗口,max_width ≈ 359.5px,而 cli.rs 内层 Resizable 的 min bound 来自 cli_subagent_width_bounds(window_width) 返回的 (MIN_RESIZABLE_WIDTH=360, ...) ——外层约束上限 < 内层 min,两者冲突。

测试 cli_subagent_resize_width_bounds_do_not_drop_below_panel_minimum 只验证了 min=max=360 的情况,没有覆盖与外层 layout max(305.6,窗口=320)的关系。

SSH 跳板机场景下用户可能在较小窗口中运行,建议在 cli_subagent_width_bounds 中接受外层布局 available width 并以此为 max 的上限,或在 CLISubagentView::render() 里把外层 layout constraint 传给 Resizable 的 bounds callback,使两者的上下限保持一致。


Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已经修复

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question:EventHandler.with_always_handle() 在滚动到底部边界时会意外解除 auto-scroll 钉住状态

CLISubagentView::render() 末尾:

let content = EventHandler::new(clipped_content)
    .with_always_handle()
    .on_scroll_wheel(|ctx, _app, _, _| {
        ctx.dispatch_typed_action(CLISubagentAction::ConversationScrollManuallyMoved);
        cli_subagent_conversation_scroll_wheel_dispatch_result()
    })
    .finish();

NewScrollable 设置了 with_propagate_mousewheel_if_not_handled(true):当内容已经滚到底部,再向下滚动时,scroll 事件「未被消费」→ 向上冒泡 → 外层 EventHandler 触发 → ConversationScrollManuallyMoved 被 dispatch → is_conversation_scroll_pinned_to_bottom = false

场景:内容正在流式输出(持续 auto-scroll 到底),用户下意识再往下滚一下(已在底部)→ 解钉 → 新输出不再自动跟踪,用户需手动拖回底部。

是否可以检测「scroll 事件实际上是向下的且内容已在底部」时不解钉?或者在 ConversationScrollManuallyMoved handler 中加一个「当前是否已在底部」的判断?


Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已经修复

Copy link
Copy Markdown
Owner

Review 总结

倾向:Comment(非阻塞,建议处理 scroll 边界行为后合并)

功能目标(SSH 跳板机模式下 CLI agent 浮窗可拖拽、可查看对话历史)在核心逻辑上实现完整:history_models / history_exchange_ids 去重机制正确,handle_updated_exchange_output 增加 exchange_id 守卫防止过期回调污染当前状态,AppendedExchange 事件处理和 Resizable 布局整合均符合预期。

阻塞问题

无严格阻塞,但以下两点建议修复后合并:

  1. 极窄窗口布局冲突(见 block_list_element.rs inline comment)
    cli_subagent_layout_max_size 返回的 max_width 在窗口宽度 ≤ ~375px 时会小于内层 Resizablemin(360px),导致外层约束上限 < 内层下限,布局行为未定义。在 SSH 小终端场景中有一定概率触发。

  2. 滚动至底部边界时意外解除 auto-scroll(见 cli.rs inline comment)
    EventHandler.with_always_handle() 会在 NewScrollable 内容已在底部时把 scroll 事件冒泡并触发 ConversationScrollManuallyMoved,解除 is_conversation_scroll_pinned_to_bottom。在内容流式输出期间,用户向下"确认底部"的滚动会让自动跟踪失效。

建议

  • 历史轮次的 text_section_index 跨 model 累计(而 code_section_index 等是 per-model)是有意设计(保证链接/脱敏索引连续),建议加一行注释说明意图,避免后续维护者认为这是笔误。
  • render_framed_container 替代 render_scrollable_container 的重构思路清晰,把单一整体 scroll 下推到最外层是正确方向。

看起来不错的地方

  • 测试覆盖到位:bounds 函数、history 去重、scroll pin/unpin、wheel propagation 均有单元测试
  • models_to_render 的分支(empty history → 只用 current model;否则用 history)实现了向后兼容的渐进式升级,不破坏没有 follow-up 的普通对话
  • BYOP fallback 路径从日志"replace"改为"切换",措辞更准确

— 由 Claude Routine 自动生成;如需复评请 @ 维护者人工触发。


Generated by Claude Code

@mrbbbaixue

Copy link
Copy Markdown

ssh连跳板机模式下agent cli
这个场景具体指的是什么,不是很看得懂🤔
有没有截图

@zerx-lab

Copy link
Copy Markdown
Owner

维护者复审:FIX-NEEDED(阻塞,涉及安全)

可缩放浮窗 + 历史保留的整体实现是站得住的(resize 钳制正确、滚轮事件派发顺序正确、AIBlockModelImpl 为轻量句柄无内存泄漏、无新增 TerminalModel::lock() 死锁风险)。但发现一个密钥脱敏(secret redaction)索引错位的阻塞缺陷:

根因 app/src/ai/blocklist/block/cli.rs:1177:1280-1282:1301

  • 重写后的 render 把 text_section_index 声明在 history_models 循环(1177),跨所有历史模型累加;而 code/table/image 索引在循环每个模型重置(1280-1282)。
  • 累加后的 text_section_indexstarting_text_section_index(1301)→ TextLocation::Output { section_index },用于查询脱敏覆盖层(view_impl/common.rs:1561、1645secrets_for_location)。
  • 但脱敏状态只对最新模型输出按 0 基计算(cli.rs:978-980secret_redaction.rs:565-587;handle_updated_exchange_outputexchange_id 门控只跑最新模型)。

后果(当 history_models.len() ≥ 2、前轮有文本输出、且开启脱敏 safe mode / 企业脱敏时):

  1. 最新一轮输出以 offset 后的 section_index 渲染 → secrets_for_location 返回 None → 最新一轮密钥明文泄露;
  2. 较早轮(0 基)可能命中最新轮的 secret byte_range,把错误的覆盖层套到不匹配的文本上。
  • 对照:query 脱敏已正确做成连续累加(set_state_from_updated_inputsinput_index),唯独 output 脱敏漏改。

修复方案(二选一):

  • (a) 让 output 脱敏覆盖全部 history_models,与 query 一样用累加的 section_index 重算;
  • (b) 把 text_section_index 改为每模型重置,且仅对最新模型传入真实 SecretRedactionState、history 模型传入空状态(避免错误命中)。

请补充对应的单元/集成测试(多轮 + 开启脱敏)覆盖该路径后再合并。本仓库无 PR CI,review 是唯一门槛,这一项涉及密钥泄露,必须先修复。

@Ghostknight0

Copy link
Copy Markdown
Author

ssh连跳板机模式下agent cli 这个场景具体指的是什么,不是很看得懂🤔 有没有截图

image

@Ghostknight0

Copy link
Copy Markdown
Author

维护者复审:FIX-NEEDED(阻塞,涉及安全)

可缩放浮窗 + 历史保留的整体实现是站得住的(resize 钳制正确、滚轮事件派发顺序正确、AIBlockModelImpl 为轻量句柄无内存泄漏、无新增 TerminalModel::lock() 死锁风险)。但发现一个密钥脱敏(secret redaction)索引错位的阻塞缺陷:

根因 app/src/ai/blocklist/block/cli.rs:1177:1280-1282:1301

  • 重写后的 render 把 text_section_index 声明在 history_models 循环(1177),跨所有历史模型累加;而 code/table/image 索引在循环每个模型重置(1280-1282)。
  • 累加后的 text_section_indexstarting_text_section_index(1301)→ TextLocation::Output { section_index },用于查询脱敏覆盖层(view_impl/common.rs:1561、1645secrets_for_location)。
  • 但脱敏状态只对最新模型输出按 0 基计算(cli.rs:978-980secret_redaction.rs:565-587;handle_updated_exchange_outputexchange_id 门控只跑最新模型)。

后果(当 history_models.len() ≥ 2、前轮有文本输出、且开启脱敏 safe mode / 企业脱敏时):

  1. 最新一轮输出以 offset 后的 section_index 渲染 → secrets_for_location 返回 None → 最新一轮密钥明文泄露;
  2. 较早轮(0 基)可能命中最新轮的 secret byte_range,把错误的覆盖层套到不匹配的文本上。
  • 对照:query 脱敏已正确做成连续累加(set_state_from_updated_inputsinput_index),唯独 output 脱敏漏改。

修复方案(二选一):

  • (a) 让 output 脱敏覆盖全部 history_models,与 query 一样用累加的 section_index 重算;
  • (b) 把 text_section_index 改为每模型重置,且仅对最新模型传入真实 SecretRedactionState、history 模型传入空状态(避免错误命中)。

请补充对应的单元/集成测试(多轮 + 开启脱敏)覆盖该路径后再合并。本仓库无 PR CI,review 是唯一门槛,这一项涉及密钥泄露,必须先修复。

已提交新的修复,请重新审核

wushijian added 3 commits June 30, 2026 12:48
SSH 跳板机场景的 agent CLI 浮窗走 AltScreenElement 路径,浮窗内多轮
对话气泡(query/output/action)平铺在同一个 Flex::column 里,每个气泡是
独立的 SelectableArea。

根因:
- Flex::dispatch_event 会把同一个鼠标事件广播给所有子元素,命中不 break;
- SelectableArea::dispatch_event 在 LeftMouseDown/LeftMouseUp 时无条件调用
  invoke_selection_handler,即使鼠标不在该气泡内(框架为"点击空白清选"设计);
- cli.rs 回调入口无条件调用 clear_selection_handles_for_active_area,会清掉
  同组其它 index 的 handle。

三者叠加,导致在 block N 上 down/up 时,前面未命中气泡的回调先执行,用自己
的 index 把 block N 的选择清掉:
- 前/中 block:被后续未命中气泡的回调误清 → 无法保持选中;
- 最后一个 block:LeftMouseUp 广播时前面的气泡先清掉它,松手即失选。

修复:在三处 SelectableArea 回调(query/output/action)入口加 is_this_area_active
判定 —— 只有本区域确实参与选择时(is_selecting() 为 true 或 selection 非空)
才清掉其它同级区域的旧选择。Flex 广播给未命中气泡的回调不再误清命中区域。

该修复对 alt-screen(SSH 跳板机)和 block_list(主对话区)两条路径都生效,
不触碰框架层(selectable_area.rs / flex)与现有 WIP。
之前主对话区(block_list 路径)在开启 "Hide responses" 时只隐藏 AI
输出,仍保留用户提问气泡,导致界面只藏输出不藏输入,与 CLI 浮窗
(commit 6878548)的行为不一致。

新增 common::should_render_query_and_header,在原有判断基础上增加
!should_hide_responses 条件,view_impl 在渲染 query/header 以及
contains_user_query_and_is_not_pin_to_top 判断时统一使用该函数。

带 2 个单元测试覆盖隐藏/显示两种场景。
@Ghostknight0

Copy link
Copy Markdown
Author

对气泡的一些交互做了更多细节修正

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants