尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Kilo JetBrains 前端重构:用共享 PickerPopup 基类收敛两个模型选择弹窗

Kilo JetBrains 前端重构:用共享 PickerPopup 基类收敛两个模型选择弹窗 Kilo JetBrains 前端重构用共享 PickerPopup 基类收敛两个模型选择弹窗【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode本篇技术指南以仓库内 consolidate-jetbrains-model-pickers 计划文档 为骨架讲解 Kilo JetBrains 插件前端如何把两个高度重复的列表选择弹窗——会话界面的「Prompt 模型选择器」与设置页自定义 Provider 的「模型多选弹窗」——收敛为一个配置驱动的泛型基类PickerPopupT并配套抽取渲染基类PickerListRendererT。读者读完后将掌握这套「薄配置 泛型弹窗 可扩展渲染器」的组件化重构思路理解其配置参数、键盘/鼠标交互约定、选择状态归属与测试策略并能对照当前仓库源码该计划已在packages/kilo-jetbrains/frontend/落地直接复用或迁移这套模式。背景两处重复的列表弹窗实现在重构之前JetBrains 前端存在两个功能相近但实现各自为政的列表弹窗重复了弹窗构建、背景色、头部、命中测试、键盘绑定、滚动与尺寸计算等一大段 UI 基建代码Prompt 模型选择器——位于 ModelPicker.kt 的showPopup单选提交、带搜索框、支持展开/收起详情面板、带分区Favorites / Recommended / 各 Provider 分组、每行右侧有收藏星标切换。自定义 Provider 模型选择器——位于 ProvidersSettingsUi.kt 的CustomProviderDialog.showModelPopup多选切换、选中的模型写入对话框的models文本框。两个弹窗的核心差异点包括单选 vs 多选、有无搜索框、有无详情/展开面板、有无每行尾部的收藏点击区、有无头部工具栏按钮Select All / Deselect All。重构目标就是把差异参数化让两个调用方都变成一份「薄配置」。计划明确限定了改造范围全部工作集中在packages/kilo-jetbrains/frontend/不涉及任何后端 / RPC / DTO 改动且被改造的文件均为 Kilo 自有代码无kilocode_change上游标记。从当前仓库源码看该计划已完整落地新增了ai/kilocode/client/ui/picker/包包含 PickerPopup.kt 与 PickerListRenderer.kt。总体思路与已定决策计划的几个关键决策决定了整个架构形态抽取泛型基类PickerPopupT两个弹窗退化为薄配置所有公共行为收进基类。选择状态由调用方持有基类本身是「无选择状态」的——它只通过checked(row)渲染勾选图标通过onPrimary(row)响应用户激活。因此 Prompt 侧继续用 favorites 回调维护收藏Provider 侧继续把models文本框当作唯一事实来源。Select All / Deselect All 变为头部按钮由调用方提供两个按钮组件放入头部工具栏删除原先列表内的CustomModelRow.selectAll特殊行。渲染器同步抽取基类ModelPickerRenderer与 Provider 渲染器共同继承PickerListRenderer收藏星标的点击热区被泛化为通用的「尾部点击区」辅助函数。新增包ai/kilocode/client/ui/picker/承载PickerPopupT与PickerListRendererTPickerRow与ModelSearch保持原位被复用。PickerPopup 基类配置驱动的通用弹窗构造参数总览PickerPopupT的全部能力都由构造参数或成员变量暴露默认值贴合原ModelPicker的既有行为。下表结合 PickerPopup.kt 的实际实现整理参数类型 / 默认值语义anchorJComponent弹窗锚点组件。Prompt 用按钮自身ABOVE/BELOWProvider 用pick按钮UNDERNEATHplacementPlacementABOVE/BELOW/UNDERNEATHABOVE走PopupShowOptions.aboveComponent(anchor)BELOW/UNDERNEATH走showUnderneathOf(anchor)rows(query: String) - ListT搜索文本变化时重建行数据Provider 原计划传入忽略query的函数modelCollectionListModelT列表数据模型由基类与调用方共享供refresh()重建rendererPickerListRendererT行渲染器key(T) - Any?行稳定标识用于refresh(prefer...)时恢复高亮modeModeSingle/Multi单选提交即关闭多选切换后保持打开同时派生autoCloseautoCloseBoolean默认mode Single主点击后是否关闭弹窗false时底部会出现 Close 按钮onPrimary(T) - Unit主操作回调。Prompt 在回调内部依据row.item null决定激活或清除Provider 切换文本框中的模型成员sectionTitle(ListT, Int) - String?默认{ _, _ - null }分区标题Prompt 传modelPickerSectionTitleProvider 传 nulltrailingHit/onTrailing((JList*, Rectangle, Point) - Boolean)?/((T) - Unit)?可选的尾部点击区如收藏星标热区。键盘上Single 模式按ShiftSPACE触发searchBoolean默认false是否在头部 CENTER 显示SearchTextFieldtoolbarListJComponent默认空头部 WEST 的额外按钮Provider 传入 Select All / Deselect Alldetails/onPreview/expandStateKeyJComponent?/(T?) - Unit/String?非空时显示展开HoverIcon与 EAST 详情面板用PropertiesComponent持久化展开状态details is Disposable时通过Disposer.register(popup, details)防泄漏minWidth/maxWidth/maxVisibleRows/emptyListHeight420/760/10/120尺寸参数。Provider 传minWidth 320emptyText字符串默认model.picker.no.matches空列表占位文案弹窗骨架与公共标志基类在show()中统一完成安装搜索、键盘、鼠标与展开监听ListUtil.installAutoSelectOnMouseMove(list)与ScrollingUtil.installActions(list)挂载自动选中与滚动然后组装contentBorderLayoutNORTH 头部、CENTER 滚动列表、SOUTH 可选 footer、EAST 可选详情最后通过createComponentPopupBuilder构建并展示.createComponentPopupBuilder(content, field?.textEditor ?: list) .setRequestFocus(true) .setFocusable(true) .setCancelOnClickOutside(true) .setCancelKeyEnabled(true) .setCancelOnWindowDeactivation(true) .setLocateWithinScreenBounds(true) .setResizable(false) .setMovable(false)即请求焦点、可聚焦、点击外部取消、ESC 可取消、窗口失活取消、限制在屏幕范围内、不可缩放、不可拖动。同时popupBackground辅助函数被移入ui/picker包内统一维护internal val popupBackground: Color get() if (NewUI.isEnabled()) JBUI.CurrentTheme.Popup.BACKGROUND else UIUtil.getListBackground()列表、滚动视口、头部与详情面板全部使用该背景并调用PopupUtil.applyNewUIBackground(list)与AbstractPopup.customizeSearchFieldLook(it, true)保证新旧 UI 外观一致。头部布局头部是一个BorderLayout面板WEST 放toolbar按钮水平 Stack 排列CENTER 放搜索框searchfalse时为空EAST 放展开HoverIcon仅当 details 存在时从而保持了AbstractPopup.customizeSearchFieldLook与背景接线。展开图标来自/icons/expand.svg与/icons/collapse.svg随状态切换并同步 tooltip 与 accessible name。refresh()是基类对外暴露的关键方法它依据rows(field?.text)重建数据、replaceAll进CollectionListModel然后按prefer键默认当前选中行恢复高亮找不到时回退到第一行——源码注释特意对比了ActiveListView列表选择器必须始终给出一个可接受候选项因此不会出现「记忆行消失后选中态为空」的情况。收藏切换后 Prompt 侧正是通过popup?.refresh(prefer item.key); popup?.repaint()完成即时刷新。键盘与鼠标交互约定键盘绑定同时注册在列表以及存在时的搜索编辑器上语义按模式区分按键行为UP/DOWN移动选中仅搜索框需要注册列表本身由ScrollingUtil处理ENTER对选中行执行 primary单选回调后closeOk(null)多选刷新并保持打开ESCpopup.cancel()ShiftSPACE仅 Single 且存在 trailing 时触发尾部操作如收藏切换SPACE仅 Multi对选中行执行 primary即切换选中鼠标统一走mouseReleasedUIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)先用locationToIndex解析行索引再用getCellBoundscontains校验点击落在行内若配置了trailingHit且命中则执行onTrailing并消费事件否则执行onPrimarySingle 关闭、Multi 刷新重绘。尺寸计算原先ModelPicker里的computeInitialPopupSize/computeListPreferredWidth/computeListPreferredHeight三件套被整体移入基类并参数化宽度取「列表渲染出的最大行宽」与头部/底部preferredSize.width中的较大者再钳制在JBUI.scale(minWidth)..JBUI.scale(maxWidth)区间内渲染器不可用时回退minWidth。高度对前maxVisibleRows行逐行调用渲染器求和行数超过上限时追加滚动条宽度空列表回退emptyListHeight。展开详情时在总宽上追加一份「详情宽度」等于当前弹窗宽度并让详情面板高度与滚动区对齐。PickerListRenderer可扩展的行渲染骨架PickerListRenderer.kt 是一个抽象ListCellRendererT基类构造时接收model、checked: (T) - Boolean、sectionTitle、受保护槽位content: JComponent与可选trailing: JComponent。它负责统一行外观勾选图标列AllIcons.Actions.Checked/ 等尺寸EmptyIcon由checked(row)决定分区分隔条复用GroupHeaderSeparator作为顶部面板top/sep接线沿袭自原ModelPickerRenderersep.setHideLine(index 0)隐藏首行分隔线行布局PickerRowSelectablePanel子类包裹[check | content]可选 trailing 传入wrap.setContent(row, trailing)行内边距为UiStyle.Gap.md()/lg()/md()/pad()透明背景统一交给UiStyle.Components.transparent。子类只需实现update(...)完成自身内容填充。伴生对象还提供了泛化的尾部点击区工具fun trailingClickZone(list: JList*, bounds: Rectangle, point: Point, width: Int): Boolean { val size JBUI.scale(width) val inset trailingInset(list) if (list.componentOrientation.isLeftToRight) { val right bounds.x bounds.width - inset return point.x in (right - size)..right } val left bounds.x inset return point.x in left..(left size) }trailingInset会叠加Popup.Selection.LEFT_RIGHT_INSET与 New UI 的innerInsets并感知 RTL 方向——这正是原ModelPickerRenderer.isFavoriteClick逻辑FAVORITE_CLICK_AREA_WIDTH 32的泛化点击区域几何行为与既有测试保持一致。落地一Prompt 模型选择器Single 模式重构后的ModelPicker.showPopup()在 ModelPicker.kt 中变成一份PickerPopupModelPickerRow配置mode Mode.SingleonPrimary内部按row.item null决定clear()还是activate(item)对应空行「Not set」与正常模型rows { q - modelPickerRows(items, favorites(), q, allowEmpty, emptyText, includeSmall) }搜索时经ModelSearch的缩写/分词匹配过滤checked { it.key selected?.key }单选以当前激活模型为勾选依据sectionTitle ::modelPickerSectionTitleFavorites / Recommended / Provider 分组见 ModelPickerRows.kttrailing 复用ModelPickerRenderer.isFavoriteClick内部委托给PickerListRenderer.trailingClickZone点击触发onFavoriteToggle后经refreshFavorite回调刷新弹窗search truedetails ModelDetailsPanelexpandStateKey MODEL_PICKER_EXPANDED_KEY即kilo.model.picker.expanded展开状态持久化到PropertiesComponent尺寸沿用原常量 420 / 760 / 10 / 120。对应地ModelPickerRenderer.kt 改为继承PickerListRendererModelPickerRowcontent是「标题SimpleColoredComponent 数据收集警告warn Free/BYOK 徽标 收藏 Provider 标签」组合面板trailing是收藏星标。它的公开测试钩子DATA_COLLECTED、checked、empty伴生成员以及starIcon()、badgeVisible()、badgeText()、byokVisible()、warningVisible()、warningTooltip()内部访问器被完整保留因此 ModelPickerTest.kt 无需改动即可编译通过。同时ModelPicker的对外 APIsetItems、select、clearSelection、open、cycle、回调、Placement、Item与测试钩子selectedForTest、selectionKeyForTest、expandedForTest保持稳定PromptPanel、settings/models、settings/agents等消费方完全不受影响——这是计划明确划出的兼容性红线。落地二自定义 Provider 模型选择器Multi 模式CustomProviderDialog.showModelPopup(ids)在 ProvidersSettingsUi.kt 中同样收敛为PickerPopupString配置picker PickerPopup( anchor pick, placement PickerPopup.Placement.UNDERNEATH, rows { query - customModelRows(ids, query) }, model data, renderer CustomModelRenderer(data) { modelIds().toSet() }, mode PickerPopup.Mode.Multi, onPrimary { toggleModel(it, ids); syncActions() }, search true, toolbar listOf(select, JSeparator(SwingConstants.VERTICAL), clear), minWidth CUSTOM_MODEL_POPUP_WIDTH, // 320 maxWidth CUSTOM_MODEL_POPUP_WIDTH, maxVisibleRows CUSTOM_MODEL_POPUP_MAX_ROWS, // 10 )要点与计划的一致性/演进点CustomModelRow、customModelRows的「全选行」概念被删除PickerRow数据就是纯String模型 id 列表CustomModelRenderer是PickerListRendererString的一个小型子类content就是一个显示模型 id 的JBLabelchecked { it in selected() }sectionTitle { _, _ - null }无 trailing头部工具栏由调用方传入两个ActionLinkcustomModelsSelectAll/customModelsUnselectAll并夹一个竖向JSeparator计划原把 provider 弹窗的search定为关闭并列为 Out of scope但落地实现将其开启并让customModelRows按子串过滤——从最终代码看这成为计划之外的一项增强也证明了基类「搜索能力开箱即用」的设计价值尺寸固定 320 宽、10 行可见anchor 为pick按钮、UNDERNEATH展示。选择状态归属与可测试的纯函数多选状态始终以对话框的models文本框为唯一事实来源基类不感知任何状态。为便于单元测试计划把三类变更操作提取为CustomProviderDialog上的可见辅助方法复用既有的modelIds()/setModelIds()setModelIds内部draft null并distinct().joinToString(, )写回文本框toggleModel(id, order)把 id 加入或移出集合并按order保持原有排列顺序写回selectAllModels(ids)一次性写入全部 idclearModels()清空。两个工具栏按钮分别绑定selectAllModels(ids)与clearModels()点击后调用picker.repaint() syncActions()同步勾选态与 OK 按钮可用性。由于这些方法只操作文本框字符串无需真实弹窗即可测试。测试策略计划的测试策略遵循「直接测试真实实现、不打桩」的包内 AGENTS 约定按三类处理保持原样通过ModelPickerTest.kt 覆盖行构建器modelPickerRows、分区顺序、ModelSearch缩写过滤、modelCycle循环顺序、渲染器徽标/警告/收藏星标、以及收藏点击区的 LTR/RTL 几何ModelPickerRenderer.isFavoriteClick委托trailingClickZone后符号依然有效。更新ProvidersSettingsUiTest.kt原「列表以 select all 行开头」的用例随行删除而废弃替换为对toggleModel添加/移除 id、selectAllModels写全量、clearModels清空的断言新增/编辑/删除 Provider 与对话框关闭用例保持不变。新增对纯几何/纯渲染的单元测试——trailingClickZone的点击区计算镜像原isFavoriteClick测试与 Provider 文本渲染器的勾选态刻意不触碰活体弹窗内部状态。验证步骤计划给出的验证入口在packages/kilo-jetbrains/目录下./gradlew typecheck全量类型检查./gradlew test可配合--tests ai.kilocode.client.session.ui.model.ModelPickerTest与--tests ai.kilocode.client.settings.providers.ProvidersSettingsUiTest迭代定位./gradlew runIde手工冒烟Prompt 侧验证搜索、方向键/回车选择、收藏星标、详情展开/收起与 placement自定义 Provider 侧验证「新增 Provider → 拉取模型 → 弹窗多选 全选/取消全选 → 保存后模型列表持久化」。风险与注意点计划明确提示了四类风险重构时必须守住API 稳定性ModelPicker公开 API 与测试钩子被多个设置页依赖只有弹窗内部实现可以搬移资源泄漏details详情面板是Disposable基类必须保持Disposer.register(popup, details)的既有注册时机尺寸差异Prompt 侧是 420/760 动态宽度 详情扩展Provider 侧固定 320——必须参数化而非硬编码保持视觉等价交互语义差异单选「提交即关闭」、多选「切换保持打开」、以及是否存在尾部操作都应在基类的Mode与参数上编码而不是在调用方打补丁式特判。小结这次收敛的本质是把「弹窗基建」与「业务选择逻辑」彻底解耦PickerPopupT负责弹窗生命周期、键盘/鼠标、搜索、分区、详情展开与尺寸计算PickerListRendererT负责行骨架、勾选列与尾部点击区两个调用方各持一份声明式配置和属于自己的状态单选激活项 /models文本框。对 Kilo JetBrains 前端而言这套模式既消除了两处弹窗的重复实现又为后续其它列表选择弹窗如计划中明确暂不纳入的ModePicker、ReasoningPicker、SessionAccountOverlay提供了即插即用的底座——新弹窗只需提供数据、渲染器和一行配置即可获得与模型选择器一致的交互与观感。【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表