JetBrains 插件空状态页 Feedback & Support 按钮实现:从 VS Code 对齐到 IntelliJ Popup 落地
【免费下载链接】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
导读
本文基于 Kilo 仓库内.kilo/plans/1780256109130-stellar-forest.md这一份 JetBrains 插件功能实现计划展开,完整讲解如何把 VS Code 端空状态页(empty state)的Feedback & Support按钮与反馈弹层移植到 JetBrains 插件,实现 GitHub Issues、Discord、Customer Support 三个跳转入口,并满足点击外部 / 按 Esc 关闭、非模态、不抢焦点等交互要求。文章不仅逐条展开计划的 6 个实施步骤(本地化、UI 扩展、Popup 构建、测试、发布说明、验证),还对照当前仓库中已落地的源码(EmptySessionFeedback.kt、EmptySessionPanel.kt、EmptySessionPanelTest.kt及 i18n 资源),给出计划与最终实现之间的差异,读者可据此在 JetBrains 插件前端复现或继续改进同类功能。
一、功能目标与整体背景
该计划的目标非常明确:把 VS Code 端空状态页上的Feedback & Support入口原样搬到 JetBrains 插件中,弹出一个**非模态(non-modal)**小弹层,提供三个与 VS Code 完全一致的目的地:
- GitHub Issues:
https://github.com/Kilo-Org/kilocode/issues/new/choose - Discord:
https://kilo.ai/discord - Customer Support:
https://kilo.ai/support
弹层必须满足两个交互约束:点击弹层外部时隐藏,按下 Escape 时隐藏。
从仓库现状看,该计划已经落地:packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionFeedback.kt完整实现了按钮、弹层内容与三个 URL 常量,且feedback.urls()返回的正是上述三个 URL;对应测试 EmptySessionPanelTest.kt 中也有test feedback popup content opens expected destinations与test feedback discord action has icon两个用例守护行为。
二、调研发现:入口定位与 VS Code 参考实现
计划的第一步是定位落点与参考实现,仓库现状印证了全部结论:
JetBrains 空状态组件位于 EmptySessionPanel.kt(计划书写时为
.../session/ui/EmptySessionPanel.kt,落地时位于ui/empty/子包)。它已经渲染了 Kilo logo、欢迎文案、最近会话列表(Recents)和Show History按钮,是添加 Feedback 按钮的正确位置。从源码可见该面板是一个BorderLayoutPanel,南侧(SOUTH)区域由Stack.vertical垂直堆叠Centerizer(historyButton)与Centerizer(feedback.button)构成,Feedback 按钮正好排在Show History之下、保持居中布局。VS Code 侧三件套为参考基准:
- 按钮:
packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx(计划标注位置) - 弹层内容:FeedbackDialog.tsx——SolidJS 组件,内含 Kilo Logo、
feedback.dialog.message文案,以及 GitHub(primary 变体 + github 图标)、Discord(secondary + discord 图标)、Support(secondary + help 图标)三个全宽按钮,外加一个 ghost 变体的 Cancel 按钮;点击任一动作通过vscode.postMessage({ type: "openExternal", url })打开外部链接并关闭弹层。 - 样式:
packages/kilo-vscode/webview-ui/src/styles/welcome.css
- 按钮:
IntelliJ Popup API已在本地 IntelliJ 源码(计划注明
$INTELLIJ_REPO)中确认可用:JBPopupFactory.getInstance().createComponentPopupBuilder(content, focusComponent)setModalContext(false):非模态上下文setRequestFocus(false)/setFocusable(false):让弹层不抢焦点setCancelOnClickOutside(true):点击外部关闭setCancelKeyEnabled(true):Esc 关闭setCancelOnWindowDeactivation(true)/setCancelOnOtherWindowOpen(true):窗口失活或其它窗口打开时清理弹层
需要特别说明的是,最终实现采用了
createBalloonBuilder而非计划中的createComponentPopupBuilder:在 EmptySessionFeedback.kt 中,弹层由JBPopupFactory.getInstance().createBalloonBuilder(content)构建,并通过.setHideOnClickOutside(true)、.setHideOnKeyOutside(true)、.setHideOnAction(true)、.setHideOnFrameResize(true)实现点击外部、键盘(Esc)、动作触发与窗口缩放时的隐藏——与计划的交互目标一致,但 API 形态不同,这是实现相对计划的一处实际偏差。平台图标(AllIcons)可用性:
- Feedback 按钮:
AllIcons.Ide.Feedback - GitHub 动作:
AllIcons.Vcs.Vendors.Github - Support/帮助动作:
AllIcons.Actions.Help或AllIcons.General.ContextHelp - Discord 没有平台图标:需要借用 VS Code / kilo-ui 现有的 Discord SVG 素材,改造成 IntelliJ 兼容的 SVG 颜色,必要时提供深色变体。仓库中已落地两个资源文件:discord.svg 与 discord_dark.svg,并通过
IconLoader.getIcon("/icons/discord.svg", EmptySessionPanel::class.java)加载。
- Feedback 按钮:
三、实施步骤详解(计划原文 + 源码对照)
步骤 1:添加本地化 Bundle Keys
计划要求在frontend/src/main/resources/messages/KiloBundle.properties添加基英文键:
| Key | 值 |
|---|---|
feedback.button | Feedback and Support |
feedback.dialog.message | We'd love to hear your feedback or help with any issues you're experiencing. |
feedback.dialog.github | Report an issue on GitHub |
feedback.dialog.discord | Join our Discord community |
feedback.dialog.support | Customer Support |
现有 Cancel 文案若已存在则复用,否则仅在必要时新增common.cancel=Cancel。仓库现状确认这些键已写入 KiloBundle.properties(第 54–58 行),且消息目录下已存在 18 个本地化变体文件(KiloBundle_ar.properties、KiloBundle_zh_CN.properties、KiloBundle_ja.properties、KiloBundle_ko.properties、KiloBundle_de.properties、KiloBundle_fr.properties等),计划要求在能力允许范围内复用 VS Code i18n 的既有翻译,并以英文回退兜底。实际按钮文案在实现中通过KiloBundle.message("feedback.button")读取,测试test feedback button uses localized text and icon也据此断言。
步骤 2:扩展 EmptySessionPanel UI
计划对新增按钮提出了明确的外观规格,要求与 VS Code 概念对齐:
- 反馈图标 +
Feedback & Support文本 - 虚线、链接色边框
- 透明背景
- 手型光标
- 悬停时填充链接色并反转前景/背景以形成对比
样式必须使用 IntelliJ 主题 API 而非硬编码颜色:链接色用JBUI.CurrentTheme.Link.Foreground.ENABLED、悬停可用...HOVERED;背景取自既有会话样式或UiStyle.Colors.editorBackground();间距用UiStyle.Gap/JBUI.Borders;圆角在绘制时用UiStyle.Arc.component()或JBUI.getInt("Button.arc", 6)。按钮需放在Show History下方、保持居中布局,Swing 变更保持在 EDT 上执行并维持 retained-component 行为。
落地实现中,EmptySessionPanel.kt 的做法是让FeedbackButton直接继承面板内部的ShowHistoryButton(一个自定义JButton子类),因此天然继承了其既有行为:isFocusable = false、isContentAreaFilled = false、isBorderPainted = false、isOpaque = false、手型光标,以及paintComponent中基于JBUI.CurrentTheme.ActionButton.hoverBackground()与JBUI.getInt("Button.arc", 6)的圆角悬停填充。feedback.button在构造时挂上addActionListener { popup() },并作为Centerizer包裹项加入南侧 actions 栈(见上文源码第 153–159 行)。
步骤 3:实现 Feedback Popup(前端 Swing 代码)
计划给出的弹层内容结构,与 VS Code 弹层逐一对应:
- 顶部 Kilo logo,使用既有
/icons/kilo-content.svg - 消息文案
- GitHub、Discord、Customer Support 三个动作按钮/行,且每个动作都必须有图标:
- GitHub →
AllIcons.Vcs.Vendors.Github - Discord → 借用的 Discord SVG
- Customer Support →
AllIcons.Actions.Help或AllIcons.General.ContextHelp
- GitHub →
- 可选 Cancel 按钮(Esc / 点击外部已能关闭)
弹层构建(计划版)配置项如下,均来自 IntelliJ Popup API:
JBPopupFactory.createComponentPopupBuilder(content, null) .setModalContext(false) // 非模态 .setRequestFocus(false) // 不请求焦点 .setFocusable(false) // 不可聚焦(除非需要 Tab 遍历) .setCancelOnClickOutside(true) // 点击外部关闭 .setCancelKeyEnabled(true) // Esc 关闭 .setCancelOnWindowDeactivation(true) // 窗口失活清理 .setCancelOnOtherWindowOpen(true) // 其他窗口打开清理 .setResizable(false) .setMovable(false) .showUnderneathOf(feedbackButton) // 展示在按钮下方打开链接统一使用BrowserUtil.browse(url),并在打开 URL 后关闭弹层。Discord 图标资源按插件既有命名模式放入frontend/src/main/resources/icons/(如discord.svg与discord_dark.svg),用IconLoader.getIcon("/icons/discord.svg", EmptySessionPanel::class.java)或小型 Kilo 自有图标对象加载。
源码现状对照:EmptySessionFeedback.kt 的content(open)静态方法完整实现了上述结构:顶部 logo(点击打开https://kilocode.ai,且为手型光标)、居中消息、三个ActionButton竖排堆叠(GitHub 用AllIcons.Vcs.Vendors.Github、Discord 用DISCORD_ICON、Support 用AllIcons.Actions.Help)。popup()方法采用createBalloonBuilder变体实现(见第二节的差异说明),调用链为:点击按钮 →popup()→ 若已有弹层则先hide()→ 构建 content →popup.show(point, Balloon.Position.below)展示在按钮正下方;点击动作行时执行注入的browse(url)回调(生产环境为BrowserUtil::browse,测试环境可注入假实现)并hide()。弹层通过Disposer与面板生命周期绑定,dispose()时隐藏弹层,避免泄漏。
步骤 4:测试覆盖(EmptySessionPanelTest.kt)
计划要求的测试点,在 EmptySessionPanelTest.kt 中均有对应用例:
- 按钮文案使用 i18n:
test feedback button uses localized text and icon断言panel.feedbackText() == KiloBundle.message("feedback.button")且图标非空。 - 手型光标与边框语义:
test action controls use hand cursor and no show history outline断言feedbackCursor() == Cursor.HAND_CURSOR且feedbackBorderPainted()为 false。 - 可注入浏览回调:面板构造参数
browse: (String) -> Unit = BrowserUtil::browse(EmptySessionPanel.kt 第 62 行)即为测试预留的注入点,feedbackContent { opened.add(it) }把 URL 收集进内存列表,测试因此无需真正启动浏览器。 - 三个动作标签与 URL 映射:
test feedback popup content opens expected destinations通过UIUtil.uiTraverser遍历弹层内容中的所有JButton,断言三个按钮文案依次为feedback.dialog.github / feedback.dialog.discord / feedback.dialog.support的本地化值,逐个doClick()后断言收集到的 URL 与panel.feedbackUrls()完全一致(即与 VS Code 相同的三个 URL)。 - Discord 必须有图标:
test feedback discord action has icon定位文案为 Discord 的按钮并断言icon != null,防止回归成纯文本行。 - 最小模式(minimal):
test minimal mode shows only logo and feedback断言在 worktree/subagent 编辑器标签等最小化表面,仅保留 logo 与 feedback,隐藏历史、描述与最近会话;test full mode shows logo feedback history description and recents则验证完整模式。
测试基类为BasePlatformTestCase,通过FakeAppRpcApi、FakeSessionRpcApi、FakeWorkspaceRpcApi构造最小可运行环境,flush()用runBlocking { delay(100); UIUtil.dispatchAllInvocationEvents() }派发 EDT 事件。
步骤 5:发布说明(Release Note)
由于这是 JetBrains 插件的用户可见功能,计划要求新增 patch changeset(放在.changeset/,除非仓库存在 JetBrains 专属的发布说明机制取代 changesets),建议文案:
Add Feedback & Support to the JetBrains empty session screen.
步骤 6:验证命令
计划给出的验证流程,均在packages/kilo-jetbrains/目录下执行:
# 1) 聚焦前端/JetBrains 测试 ./gradlew :frontend:test --tests "ai.kilocode.client.session.ui.EmptySessionPanelTest" # 2) JetBrains 类型检查 bun run typecheck # 或 ./gradlew typecheck若仅改动前端 UI 且测试通过,则无需后端/SDK 代码生成。测试全名中的类已按落地后的包路径ai.kilocode.client.session.ui.EmptySessionPanelTest核对无误。
四、约束与边界(Notes)
计划明确划定了实现边界,防止范围蔓延:
- 不得引入 Kotlin UI DSL、Compose 或 JCEF:弹层必须用 Swing + IntelliJ Platform API 完成,保持与既有前端技术栈一致。
- 不得修改 shared opencode 文件:本功能全部工作限制在
packages/kilo-jetbrains/内(外加一个 changeset)。这与仓库的架构隔离规则相符。 - 优先使用平台图标,但 Discord 必须显式借用/新增图标资源,因为
AllIcons不提供 Discord 图标,而弹层需要在视觉上与 VS Code 的按钮集保持一致。
五、实现现状小结
对照仓库当前状态,这份计划已完整落地,且实现细节与计划存在两处值得关注的差异(均为从源码结构观察到的结论):
- Popup API 形态不同:计划推演的是
createComponentPopupBuilder系列 API,最终实现改用JBPopupFactory.createBalloonBuilder,借助 Balloon 的setHideOnClickOutside(true)、setHideOnKeyOutside(true)、setHideOnAction(true)、setHideOnFrameResize(true)达成同样的“点击外部/Esc 关闭、动作后关闭”语义,并通过Disposer.register(popup) { balloon = null }维护弹层引用生命周期。 - 路径与包结构变化:计划中的
EmptySessionPanel.kt实际落在.../session/ui/empty/子包,测试类名与./gradlew过滤参数相应更新。
对于希望复刻或扩展该功能的开发者,可沿这条阅读路径深入:EmptySessionPanel.kt(按钮挂载与 retained-component 行为)→ EmptySessionFeedback.kt(弹层内容与 Balloon 构建)→ EmptySessionPanelTest.kt(行为契约与 URL 映射)→ KiloBundle.properties(文案键)→ discord.svg(借用的图标资源),并可与 VS Code 侧的 FeedbackDialog.tsx 对照理解两端交互与视觉的一致性目标。
【免费下载链接】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),仅供参考