diff --git a/.changeset/stellar-forest-feedback.md b/.changeset/stellar-forest-feedback.md new file mode 100644 index 0000000000..98dd0d17c1 --- /dev/null +++ b/.changeset/stellar-forest-feedback.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Add Feedback & Support to the JetBrains empty session screen. diff --git a/.kilo/plans/1780256109130-stellar-forest.md b/.kilo/plans/1780256109130-stellar-forest.md new file mode 100644 index 0000000000..3aafa6c0dd --- /dev/null +++ b/.kilo/plans/1780256109130-stellar-forest.md @@ -0,0 +1,109 @@ +# Plan: JetBrains Feedback & Support Button + +## Goal +Add the VS Code empty-state `Feedback & Support` affordance to the JetBrains plugin, including a non-modal popup with the same three destinations: + +- GitHub issues: `https://github.com/Kilo-Org/kilocode/issues/new/choose` +- Discord: `https://kilo.ai/discord` +- Customer support: `https://kilo.ai/support` + +The popup must hide when clicking outside or pressing Escape. + +## Findings + +- The JetBrains empty state is `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt`. +- It already renders the logo, welcome copy, recents, and `Show History`; this is the right place to add the button. +- The VS Code implementation is: + - Button: `packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx` + - Popup content: `packages/kilo-vscode/webview-ui/src/components/chat/FeedbackDialog.tsx` + - Styling: `packages/kilo-vscode/webview-ui/src/styles/welcome.css` +- Local IntelliJ API source is available at `$INTELLIJ_REPO=/Users/kirillk/products/intellij-community`. +- IntelliJ popup API confirmed in local source: + - `JBPopupFactory.getInstance().createComponentPopupBuilder(content, focusComponent)` + - `setModalContext(false)` for non-modal context + - `setRequestFocus(false)` / `setFocusable(false)` if we want the popup not to steal focus + - `setCancelOnClickOutside(true)` for outside-click dismissal + - `setCancelKeyEnabled(true)` for Escape dismissal + - `setCancelOnWindowDeactivation(true)` and `setCancelOnOtherWindowOpen(true)` are available and appropriate for cleanup +- IntelliJ platform icons found in `AllIcons`: + - Feedback button: `AllIcons.Ide.Feedback` + - GitHub action: `AllIcons.Vcs.Vendors.Github` + - Support/help action: `AllIcons.Actions.Help` or `AllIcons.General.ContextHelp` + - Discord: no platform icon found. Add a Discord icon by borrowing the existing VS Code/kilo-ui Discord SVG artwork and adapting it into the JetBrains plugin resources with IntelliJ-compatible SVG colors and dark variant if needed. + +## Implementation Steps + +1. Add localized JetBrains bundle keys. + - Add base English strings to `frontend/src/main/resources/messages/KiloBundle.properties`: + - `feedback.button=Feedback & 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` + - Reuse existing cancel text if present; otherwise add `common.cancel=Cancel` only if needed. + - Add matching keys to localized `KiloBundle_*.properties` files. If accurate translations are not already available from the VS Code i18n files, copy the VS Code translations for matching locales where practical and leave English fallback only if the JetBrains bundle mechanism supports it cleanly. + +2. Extend `EmptySessionPanel` UI. + - Add a new retained `FeedbackButton`, styled to match VS Code conceptually: + - feedback icon + `Feedback & Support` text + - dashed link-colored border + - transparent background + - hand cursor + - hover state fills with link color and flips foreground/background for contrast + - Use IntelliJ theme APIs instead of raw colors: + - link color: `JBUI.CurrentTheme.Link.Foreground.ENABLED` + - hover link color: `JBUI.CurrentTheme.Link.Foreground.HOVERED` if useful + - editor/panel background from existing session style or `UiStyle.Colors.editorBackground()` / component background + - spacing via `UiStyle.Gap` / `JBUI.Borders` + - rounded arc via `UiStyle.Arc.component()` or `JBUI.getInt("Button.arc", 6)` at paint time + - Add the feedback button below the existing `Show History` button in the empty-state south area, keeping the current centered layout. + - Keep Swing mutations on EDT and preserve retained-component behavior. + +3. Implement the feedback popup in frontend Swing code. + - Add a small popup builder method/class inside `EmptySessionPanel.kt` or a new nearby UI file if the file would become too large. + - Content should mirror VS Code: + - Kilo logo at top using existing `/icons/kilo-content.svg` + - message text + - three action buttons/rows for GitHub, Discord, and Customer Support + - every action should have an icon: `AllIcons.Vcs.Vendors.Github` for GitHub, the borrowed Discord SVG for Discord, and `AllIcons.Actions.Help` or `AllIcons.General.ContextHelp` for Customer Support + - optional Cancel button if it fits the JetBrains UX; Escape/outside click already dismisses + - Add the Discord icon asset under `frontend/src/main/resources/icons/` using the plugin's existing icon naming pattern, for example `discord.svg` and `discord_dark.svg` if the borrowed asset needs separate theme variants. + - Load the Discord icon with `IconLoader.getIcon("/icons/discord.svg", EmptySessionPanel::class.java)` or a small Kilo-owned icon object if multiple call sites need it. + - Use `BrowserUtil.browse(url)` to open links. + - Close the popup after opening a URL. + - Use `JBPopupFactory.createComponentPopupBuilder(content, null)` and configure: + - `.setModalContext(false)` + - `.setRequestFocus(false)` + - `.setFocusable(false)` unless keyboard tabbing inside popup is desired; if buttons need keyboard focus, use `.setFocusable(true)` with `.setRequestFocus(false)` + - `.setCancelOnClickOutside(true)` + - `.setCancelKeyEnabled(true)` + - `.setCancelOnWindowDeactivation(true)` + - `.setCancelOnOtherWindowOpen(true)` + - `.setResizable(false)` + - `.setMovable(false)` + - Show with `popup.showUnderneathOf(feedbackButton)`. + +4. Add test coverage in `EmptySessionPanelTest.kt`. + - Assert feedback button text uses `KiloBundle.message("feedback.button")`. + - Assert feedback button has hand cursor and visible border semantics. + - Add an injectable browse callback or popup action callback only if needed for testing without launching browsers; keep it minimal and avoid production-only test hooks if component traversal can exercise enough behavior. + - Add a test that clicking the feedback button creates/shows popup content, if feasible in `BasePlatformTestCase`; otherwise test the retained popup content builder/action rows directly through package-internal methods. + - Assert the three action labels exist and URL callbacks map to the same URLs as VS Code. + - Assert the Discord action has an icon, so the plan cannot regress to a text-only Discord row. + +5. Add release note. + - This is user-facing for the JetBrains plugin, so add a patch changeset under `.changeset/` unless the repo has a JetBrains-specific release-note mechanism that supersedes changesets. + - Suggested text: `Add Feedback & Support to the JetBrains empty session screen.` + +6. Verify. + - Run the focused frontend/JetBrains tests first, preferably from `packages/kilo-jetbrains/`: + - `./gradlew :frontend:test --tests "ai.kilocode.client.session.ui.EmptySessionPanelTest"` + - Run JetBrains typecheck from `packages/kilo-jetbrains/`: + - `bun run typecheck` or `./gradlew typecheck` + - If touching only frontend UI and tests pass, no backend/SDK generation is needed. + +## Notes + +- Do not add Kotlin UI DSL, Compose, or JCEF. +- Do not modify shared opencode files; this work is entirely under `packages/kilo-jetbrains/` plus a changeset. +- Prefer platform icons where available, but explicitly borrow/add the Discord icon because `AllIcons` does not provide one and the popup should match the VS Code button set visually. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index 240d562cdc..a3aa435dbb 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -25,6 +25,7 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay @@ -346,6 +347,7 @@ class KiloBackendAppService private constructor( sessions.start(connection.api!!, connection.apiClient!!, connection.port, connection.events) chat.start(connection.apiClient!!, connection.port, connection.events) workspaces.start(connection.api!!, connection.apiClient!!, connection.port, connection.events) + startWatchingGlobalSseEvents() setTelemetry(true) captureBackend("Backend Connected", mapOf("portKnown" to "true")) captureLoad("Backend Load Completed", start, mapOf( @@ -362,7 +364,6 @@ class KiloBackendAppService private constructor( ) ) log.info("Application started — config, profile, notifications loaded") - startWatchingGlobalSseEvents() } catch (e: TimeoutCancellationException) { val err = LoadError( resource = "app", @@ -647,7 +648,7 @@ class KiloBackendAppService private constructor( synchronized(loadLock) { if (eventWatcher?.isActive == true) return log.info("Started watching global SSE events (config.updated, disposed)") - eventWatcher = cs.launch { + eventWatcher = cs.launch(start = CoroutineStart.UNDISPATCHED) { connection.events.collect { event -> when (event.type) { "global.config.updated" -> { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index c9a0ae2845..d78a843691 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -12,7 +12,7 @@ import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.client.session.model.SessionState import ai.kilocode.client.session.scroll.SessionScroll import ai.kilocode.client.session.ui.ConnectionPanel -import ai.kilocode.client.session.ui.EmptySessionPanel +import ai.kilocode.client.session.ui.empty.EmptySessionPanel import ai.kilocode.client.session.ui.LoadingPanel import ai.kilocode.client.session.ui.ReasoningPicker import ai.kilocode.client.session.ui.mode.ModePicker diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionFeedback.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionFeedback.kt new file mode 100644 index 0000000000..ad6f79d05b --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionFeedback.kt @@ -0,0 +1,133 @@ +package ai.kilocode.client.session.ui.empty + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.HAlign +import ai.kilocode.client.ui.layout.Stack +import ai.kilocode.client.ui.layout.VAlign +import ai.kilocode.client.ui.layout.align +import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.IconLoader +import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBLabel +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import com.intellij.xml.util.XmlStringUtil +import java.awt.Cursor +import java.awt.Point +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.JButton +import javax.swing.JComponent + +internal class EmptySessionFeedback( + private val browse: (String) -> Unit, +) : Disposable { + private var balloon: Balloon? = null + + val button: JButton = FeedbackButton().apply { + addActionListener { popup() } + } + + @RequiresEdt + private fun popup() { + balloon?.let { + it.hide() + return + } + + val content = content { url -> + browse(url) + balloon?.hide() + } + val point = RelativePoint(button, Point(button.width / 2, button.height + JBUI.scale(1))) + val popup = JBPopupFactory.getInstance() + .createBalloonBuilder(content) + .setHideOnClickOutside(true) + .setHideOnKeyOutside(true) + .setHideOnAction(true) + .setHideOnFrameResize(true) + .setBorderColor(UiStyle.Balloon.border()) + .setFillColor(UiStyle.Balloon.bg()) + .setBorderInsets(UiStyle.Balloon.insets()) + .setPointerSize(UiStyle.Balloon.pointer()) + .setCornerRadius(UiStyle.Balloon.arc()) + .createBalloon() + + balloon = popup + popup.setAnimationEnabled(false) + Disposer.register(popup) { balloon = null } + popup.show(point, Balloon.Position.below) + } + + private class FeedbackButton : EmptySessionPanel.ShowHistoryButton(buttonHtml(), AllIcons.Ide.Feedback) + + override fun dispose() { + balloon?.hide() + } + + private class ActionButton(text: String, icon: javax.swing.Icon, action: () -> Unit) : JButton(text, icon) { + init { + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + addActionListener { action() } + } + } + + companion object { + @RequiresEdt + fun content(open: (String) -> Unit): JComponent { + val logo = JBLabel(IconLoader.getIcon("/icons/kilo-content.svg", EmptySessionPanel::class.java)).apply { + horizontalAlignment = JBLabel.CENTER + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + open(KILO_URL) + } + }) + } + val msg = JBLabel(messageHtml()).apply { + foreground = UIUtil.getLabelForeground() + horizontalAlignment = JBLabel.CENTER + } + val actions = Stack.vertical(gap = UiStyle.Gap.sm()) + .next(ActionButton(KiloBundle.message("feedback.dialog.github"), AllIcons.Vcs.Vendors.Github) { + open(GITHUB_ISSUES_URL) + }.align(HAlign.CENTER, VAlign.CENTER)) + .next(ActionButton(KiloBundle.message("feedback.dialog.discord"), DISCORD_ICON) { + open(DISCORD_URL) + }.align(HAlign.CENTER, VAlign.CENTER)) + .next(ActionButton(KiloBundle.message("feedback.dialog.support"), AllIcons.Actions.Help) { + open(SUPPORT_URL) + }.align(HAlign.CENTER, VAlign.CENTER)) + + return Stack.vertical(gap = UiStyle.Gap.lg()) + .fill(UiStyle.Gap.sm()) + .next(logo.align(HAlign.CENTER, VAlign.CENTER)) + .next(msg.align(HAlign.CENTER, VAlign.CENTER)) + .fill(UiStyle.Gap.xs()) + .next(actions.align(HAlign.CENTER, VAlign.CENTER)) + .fill(UiStyle.Gap.xs()) + } + + fun urls() = listOf(GITHUB_ISSUES_URL, DISCORD_URL, SUPPORT_URL) + + private fun messageHtml() = XmlStringUtil.wrapInHtml( + "
${XmlStringUtil.escapeString(KiloBundle.message("feedback.dialog.message"))}
" + ) + + private fun buttonHtml() = XmlStringUtil.wrapInHtml( + XmlStringUtil.escapeString(KiloBundle.message("feedback.button")) + ) + + private const val KILO_URL = "https://kilocode.ai" + private const val GITHUB_ISSUES_URL = "https://github.com/Kilo-Org/kilocode/issues/new/choose" + private const val DISCORD_URL = "https://kilo.ai/discord" + private const val SUPPORT_URL = "https://kilo.ai/support" + private val DISCORD_ICON = IconLoader.getIcon("/icons/discord.svg", EmptySessionPanel::class.java) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt similarity index 53% rename from packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt rename to packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt index 7f8283a706..29ad9df649 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt @@ -1,31 +1,25 @@ -package ai.kilocode.client.session.ui +package ai.kilocode.client.session.ui.empty import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionActivityKind -import ai.kilocode.client.session.SessionRef -import ai.kilocode.client.session.history.HistoryActivitySnapshot -import ai.kilocode.client.session.history.HistoryTime -import ai.kilocode.client.session.history.LocalHistoryItem -import ai.kilocode.client.session.history.itemAt -import ai.kilocode.client.session.history.title +import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import ai.kilocode.client.session.ui.style.SessionUiStyle -import ai.kilocode.client.session.controller.SessionController -import ai.kilocode.client.ui.FilledBadgeIcon import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.layout.Align import ai.kilocode.client.ui.layout.HAlign +import ai.kilocode.client.ui.layout.Stack import ai.kilocode.client.ui.layout.VAlign import ai.kilocode.client.ui.layout.align import ai.kilocode.rpc.dto.SessionDto import com.intellij.icons.AllIcons +import com.intellij.ide.BrowserUtil import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.IconLoader import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBList import com.intellij.util.ui.Centerizer import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil @@ -35,19 +29,14 @@ import java.awt.BorderLayout import java.awt.Component import java.awt.Cursor import java.awt.Dimension -import java.awt.FlowLayout import java.awt.Graphics import java.awt.Graphics2D import java.awt.RenderingHints import java.awt.event.HierarchyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent -import java.awt.event.MouseMotionAdapter -import javax.swing.DefaultListModel import javax.swing.JButton -import javax.swing.JList -import javax.swing.ListCellRenderer -import javax.swing.ListSelectionModel +import javax.swing.JComponent import javax.swing.Timer /** @@ -63,51 +52,19 @@ class EmptySessionPanel( private val history: () -> Unit = {}, private val activity: () -> Map = { emptyMap() }, private val titles: () -> Map = { emptyMap() }, + private val browse: (String) -> Unit = BrowserUtil::browse, ) : BorderLayoutPanel(), Disposable, SessionEditorStyleTarget { val view: Align = align(HAlign.CENTER, VAlign.CENTER) - private val model = DefaultListModel() - private var hover = -1 - private var style = SessionEditorStyle.current() - private var snapshot = HistoryActivitySnapshot() private val timer = Timer(ACTIVITY_MS) { syncActivity() } - - private val recentTitle = JBLabel(KiloBundle.message("session.empty.recent")).apply { - foreground = UIUtil.getContextHelpForeground() - } - - private val list = JBList(model).apply { - isOpaque = false - selectionMode = ListSelectionModel.SINGLE_SELECTION - visibleRowCount = SessionUiStyle.RecentSessions.LIMIT - cellRenderer = SessionRenderer() - cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) - emptyText.clear() - addMouseListener(object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent) { - val item = itemAt(this@apply, e) ?: return - controller.openSession(SessionRef.Local(item.session)) - } - - override fun mouseExited(e: MouseEvent) { - hover = -1 - repaint() - } - }) - addMouseMotionListener(object : MouseMotionAdapter() { - override fun mouseMoved(e: MouseEvent) { - val index = index(e) - if (hover == index) return - hover = index - repaint() - } - }) - } + internal val recent = RecentsList(recents, controller) private val historyButton = ShowHistoryButton().apply { addActionListener { history() } } + private val feedback = EmptySessionFeedback(browse) + private val welcomeLabel = JBLabel(welcomeHtml()).apply { foreground = UIUtil.getContextHelpForeground() horizontalAlignment = JBLabel.CENTER @@ -132,9 +89,9 @@ class EmptySessionPanel( init { Disposer.register(parent, this) + Disposer.register(this, feedback) isOpaque = false applyStyle(SessionEditorStyle.current()) - setSessions(recents) addHierarchyListener { e -> if (e.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() == 0L) return@addHierarchyListener if (isShowing) { @@ -159,40 +116,28 @@ class EmptySessionPanel( add(description.align(HAlign.CENTER, VAlign.CENTER), BorderLayout.CENTER) } - val recent = BorderLayoutPanel().apply { - isOpaque = false - add(recentTitle, BorderLayout.NORTH) - add(list, BorderLayout.CENTER) - } - val south = BorderLayoutPanel().apply { isOpaque = false - add(Centerizer(historyButton, Centerizer.TYPE.HORIZONTAL), BorderLayout.CENTER) + add(Stack.vertical(gap = UiStyle.Gap.lg()) + .next(Centerizer(historyButton, Centerizer.TYPE.HORIZONTAL)) + .next(Centerizer(feedback.button, Centerizer.TYPE.HORIZONTAL)), BorderLayout.CENTER) } add(header, BorderLayout.NORTH) - add(recent, BorderLayout.CENTER) + if (recent.hasSessions()) add(recent, BorderLayout.CENTER) add(south, BorderLayout.SOUTH) } - private fun setSessions(sessions: List) { - model.clear() - sessions.take(SessionUiStyle.RecentSessions.LIMIT).map(::LocalHistoryItem).forEach(model::addElement) - revalidate() - repaint() - } - - internal fun recentCount() = model.size() + internal fun recentCount() = recent.count() internal fun selectRecent(index: Int) { - list.selectedIndex = index + recent.select(index) } - internal fun selectedRecent() = list.selectedIndex + internal fun selectedRecent() = recent.selected() internal fun clickRecent(index: Int) { - list.selectedIndex = index - controller.openSession(SessionRef.Local(model.getElementAt(index).session)) + recent.click(index) } internal fun clickShowHistory() { @@ -201,13 +146,23 @@ class EmptySessionPanel( internal fun showHistoryText() = historyButton.text + internal fun feedbackText() = KiloBundle.message("feedback.button") + + internal fun feedbackCursor() = feedback.button.cursor.type + + internal fun feedbackIcon() = feedback.button.icon + + internal fun feedbackBorderPainted() = feedback.button.isBorderPainted + + internal fun feedbackContent(open: (String) -> Unit = {}): JComponent = EmptySessionFeedback.content(open) + + internal fun feedbackUrls() = EmptySessionFeedback.urls() + internal fun showHistoryBorderPainted() = historyButton.isBorderPainted internal fun showHistoryCursor() = historyButton.cursor.type - internal fun recentCursor() = list.cursor.type - - internal fun recentVisible() = true + internal fun recentVisible() = recent.hasSessions() internal fun explanationText() = KiloBundle.message("session.empty.welcome") @@ -226,92 +181,25 @@ class EmptySessionPanel( internal fun activeView() = getComponent(0) internal fun text(session: SessionDto, now: Long = System.currentTimeMillis()) = - HistoryTime.relative(LocalHistoryItem(session), now) + recent.text(session, now) internal fun rendererComponent( session: SessionDto, selected: Boolean = false, hover: Boolean = false, ): Component { - val old = this.hover - this.hover = if (hover) 0 else -1 - return list.cellRenderer.getListCellRendererComponent(list, LocalHistoryItem(session), 0, selected, false).also { - this.hover = old - } + return recent.renderer(session, selected, hover) } @RequiresEdt internal fun syncActivity() { - val next = HistoryActivitySnapshot(activity(), titles()) - val changed = snapshot.changed(next) - snapshot = next - repaintRows(changed) + recent.sync(activity(), titles()) } - private fun repaintRows(ids: Set) { - if (ids.isEmpty()) return - repeat(model.size()) { index -> - if (model.getElementAt(index).id !in ids) return@repeat - list.getCellBounds(index, index)?.let(list::repaint) - } - } - - private fun index(e: MouseEvent): Int { - val idx = list.locationToIndex(e.point) - if (idx < 0) return -1 - val box = list.getCellBounds(idx, idx) ?: return -1 - if (!box.contains(e.point)) return -1 - return idx - } - - private inner class SessionRenderer : BorderLayoutPanel(), ListCellRenderer { - private val title = JBLabel() - private val badge = JBLabel().apply { - border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) - } - private val time = JBLabel() - private val head = BorderLayoutPanel().apply { - add(BorderLayoutPanel().apply { - layout = FlowLayout(FlowLayout.LEFT, 0, 0) - isOpaque = false - add(title) - add(badge) - }, BorderLayout.CENTER) - } - - init { - layout = BorderLayout(UiStyle.Gap.pad(), 0) - border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg()) - head.isOpaque = false - add(head, BorderLayout.CENTER) - add(time, BorderLayout.EAST) - } - - override fun getListCellRendererComponent( - list: JList, - value: LocalHistoryItem?, - index: Int, - selected: Boolean, - focus: Boolean, - ): Component { - val over = selected || hover == index - isOpaque = over - background = if (over) list.selectionBackground else list.background - title.foreground = if (over) list.selectionForeground else UIUtil.getLabelForeground() - time.foreground = if (over) list.selectionForeground else UIUtil.getContextHelpForeground() - title.text = value?.let { snapshot.titles[it.id] ?: title(it) } ?: "" - time.text = value?.let(HistoryTime::relative) ?: "" - setBadge(value?.id?.let(snapshot.activity::get)) - return this - } - - private fun setBadge(kind: SessionActivityKind?) { - badge.isVisible = kind != null - badge.icon = kind?.let { FilledBadgeIcon(it.label(), it.bg(), it.fg()) } - } - } - - private inner class ShowHistoryButton : JButton(KiloBundle.message("session.showHistory"), AllIcons.Vcs.History) { + internal open class ShowHistoryButton( + text: String = KiloBundle.message("session.showHistory"), + icon: javax.swing.Icon = AllIcons.Vcs.History, + ) : JButton(text, icon) { private var over = false init { @@ -359,9 +247,8 @@ class EmptySessionPanel( } override fun applyStyle(style: SessionEditorStyle) { - this.style = style welcomeLabel.font = style.regularFont - recentTitle.font = style.smallFont + recent.applyStyle(style) revalidate() repaint() } @@ -374,7 +261,3 @@ class EmptySessionPanel( const val ACTIVITY_MS = 3_000 } } - -private fun Map.changed(next: Map) = (keys + next.keys).filterTo(mutableSetOf()) { - this[it] != next[it] -} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/RecentsList.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/RecentsList.kt new file mode 100644 index 0000000000..75380c8691 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/RecentsList.kt @@ -0,0 +1,200 @@ +package ai.kilocode.client.session.ui.empty + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.SessionActivityKind +import ai.kilocode.client.session.SessionRef +import ai.kilocode.client.session.controller.SessionController +import ai.kilocode.client.session.history.HistoryActivitySnapshot +import ai.kilocode.client.session.history.HistoryTime +import ai.kilocode.client.session.history.LocalHistoryItem +import ai.kilocode.client.session.history.itemAt +import ai.kilocode.client.session.history.title +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.style.SessionUiStyle +import ai.kilocode.client.ui.FilledBadgeIcon +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.rpc.dto.SessionDto +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBList +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import com.intellij.util.ui.components.BorderLayoutPanel +import java.awt.BorderLayout +import java.awt.Component +import java.awt.Cursor +import java.awt.FlowLayout +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.awt.event.MouseMotionAdapter +import javax.swing.DefaultListModel +import javax.swing.JList +import javax.swing.ListCellRenderer +import javax.swing.ListSelectionModel + +internal class RecentsList( + sessions: List, + private val controller: SessionController, +) : BorderLayoutPanel(), SessionEditorStyleTarget { + private val model = DefaultListModel() + private var hover = -1 + private var snapshot = HistoryActivitySnapshot() + + private val title = JBLabel(KiloBundle.message("session.empty.recent")).apply { + foreground = UIUtil.getContextHelpForeground() + } + + internal val list = JBList(model).apply { + isOpaque = false + selectionMode = ListSelectionModel.SINGLE_SELECTION + visibleRowCount = SessionUiStyle.RecentSessions.LIMIT + cellRenderer = Renderer() + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + emptyText.clear() + addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + val item = itemAt(this@apply, e) ?: return + controller.openSession(SessionRef.Local(item.session)) + } + + override fun mouseExited(e: MouseEvent) { + hover = -1 + repaint() + } + }) + addMouseMotionListener(object : MouseMotionAdapter() { + override fun mouseMoved(e: MouseEvent) { + val index = index(e) + if (hover == index) return + hover = index + repaint() + } + }) + } + + init { + isOpaque = false + add(title, BorderLayout.NORTH) + add(list, BorderLayout.CENTER) + setSessions(sessions) + } + + fun count() = model.size() + + fun hasSessions() = model.size() > 0 + + fun select(index: Int) { + list.selectedIndex = index + } + + fun selected() = list.selectedIndex + + fun click(index: Int) { + list.selectedIndex = index + controller.openSession(SessionRef.Local(model.getElementAt(index).session)) + } + + fun text(session: SessionDto, now: Long = System.currentTimeMillis()) = + HistoryTime.relative(LocalHistoryItem(session), now) + + fun renderer( + session: SessionDto, + selected: Boolean = false, + hover: Boolean = false, + ): Component { + val old = this.hover + this.hover = if (hover) 0 else -1 + return list.cellRenderer.getListCellRendererComponent(list, LocalHistoryItem(session), 0, selected, false).also { + this.hover = old + } + } + + @RequiresEdt + fun sync(activity: Map, titles: Map) { + val next = HistoryActivitySnapshot(activity, titles) + val changed = snapshot.changed(next) + snapshot = next + repaintRows(changed) + } + + override fun applyStyle(style: SessionEditorStyle) { + title.font = style.smallFont + revalidate() + repaint() + } + + private fun setSessions(sessions: List) { + model.clear() + sessions.take(SessionUiStyle.RecentSessions.LIMIT).map(::LocalHistoryItem).forEach(model::addElement) + revalidate() + repaint() + } + + private fun repaintRows(ids: Set) { + if (ids.isEmpty()) return + repeat(model.size()) { index -> + if (model.getElementAt(index).id !in ids) return@repeat + list.getCellBounds(index, index)?.let(list::repaint) + } + } + + private fun index(e: MouseEvent): Int { + val idx = list.locationToIndex(e.point) + if (idx < 0) return -1 + val box = list.getCellBounds(idx, idx) ?: return -1 + if (!box.contains(e.point)) return -1 + return idx + } + + private inner class Renderer : BorderLayoutPanel(), ListCellRenderer { + private val title = JBLabel() + private val badge = JBLabel().apply { + border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) + } + private val time = JBLabel() + private val head = BorderLayoutPanel().apply { + add(BorderLayoutPanel().apply { + layout = FlowLayout(FlowLayout.LEFT, 0, 0) + isOpaque = false + add(title) + add(badge) + }, BorderLayout.CENTER) + } + + init { + layout = BorderLayout(UiStyle.Gap.pad(), 0) + border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg()) + head.isOpaque = false + add(head, BorderLayout.CENTER) + add(time, BorderLayout.EAST) + } + + override fun getListCellRendererComponent( + list: JList, + value: LocalHistoryItem?, + index: Int, + selected: Boolean, + focus: Boolean, + ): Component { + val over = selected || hover == index + isOpaque = over + background = if (over) list.selectionBackground else list.background + title.foreground = if (over) list.selectionForeground else UIUtil.getLabelForeground() + time.foreground = if (over) list.selectionForeground else UIUtil.getContextHelpForeground() + title.text = value?.let { snapshot.titles[it.id] ?: title(it) } ?: "" + time.text = value?.let(HistoryTime::relative) ?: "" + setBadge(value?.id?.let(snapshot.activity::get)) + return this + } + + private fun setBadge(kind: SessionActivityKind?) { + badge.isVisible = kind != null + badge.icon = kind?.let { FilledBadgeIcon(it.label(), it.bg(), it.fg()) } + } + } +} + +private fun Map.changed(next: Map) = (keys + next.keys).filterTo(mutableSetOf()) { + this[it] != next[it] +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 3f9b680cf4..be96983dfb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -33,6 +33,20 @@ object UiStyle { fun component() = com.intellij.util.ui.JBValue.UIInteger("Component.arc", 8).get() } + /** Platform balloon styling used by lightweight contextual overlays. */ + object Balloon { + fun bg(): Color = UIUtil.getPanelBackground() + + fun border(): Color = JBUI.CurrentTheme.Popup.borderColor(true) + + /** New UI parameter-info balloon insets: symmetric vertical padding with wider sides. */ + fun insets() = JBUI.insets(6, 12, 6, 12) + + fun pointer() = JBUI.size(16, 8) + + fun arc() = JBUI.scale(8) + } + /** Theme-aware colors and color math used by multiple UI surfaces. */ object Colors { fun bg(): Color = UIUtil.getPanelBackground() diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/discord.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/discord.svg new file mode 100644 index 0000000000..6b1bcab573 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/discord.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/discord_dark.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/discord_dark.svg new file mode 100644 index 0000000000..db15066d32 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/discord_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index a4f6797017..3e547a9b36 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -11,6 +11,11 @@ session.account.switcher=Switch account session.empty.loading=Loading... session.empty.recent=RECENT session.showHistory=Show History +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 session.scroll.bottom=Scroll to bottom session.scroll.question=Scroll to question session.tab.new=New Session diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties index 9cb9ee7318..26ff25a94a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code هو مساعد برمجة بالذكاء الا session.empty.loading=جاري التحميل… session.empty.recent=الحديثة session.showHistory=عرض السجل +feedback.button=التغذية الراجعة والدعم +feedback.dialog.message=يسعدنا سماع تعليقاتك أو مساعدتك في حل أي مشكلات تواجهها. +feedback.dialog.github=الإبلاغ عن مشكلة على GitHub +feedback.dialog.discord=الانضمام إلى مجتمع Discord +feedback.dialog.support=دعم العملاء session.scroll.bottom=التمرير إلى الأسفل session.tab.new=جلسة جديدة session.tab.untitled=جلسة بدون عنوان diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties index a310da70e8..df6bad0d4c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code je AI asistent za kodiranje. Zatražite od njega session.empty.loading=Učitavanje… session.empty.recent=NEDAVNO session.showHistory=Prikaži historiju +feedback.button=Povratne informacije i podrška +feedback.dialog.message=Voljeli bismo čuti vaše povratne informacije ili pomoći s problemima koje doživljavate. +feedback.dialog.github=Prijavite problem na GitHubu +feedback.dialog.discord=Pridružite se našoj Discord zajednici +feedback.dialog.support=Korisnička podrška session.scroll.bottom=Skrolaj na dno session.tab.new=Nova sesija session.tab.untitled=Sesija bez naslova diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties index 706e2faf0b..2ad11f62a9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code er en AI-kodningsassistent. Bed den om at bygge session.empty.loading=Indlæser… session.empty.recent=SENESTE session.showHistory=Vis historik +feedback.button=Feedback & support +feedback.dialog.message=Vi vil gerne høre din feedback eller hjælpe med eventuelle problemer, du oplever. +feedback.dialog.github=Rapportér et problem på GitHub +feedback.dialog.discord=Deltag i vores Discord-fællesskab +feedback.dialog.support=Kundesupport session.scroll.bottom=Rul til bunden session.tab.new=Ny session session.tab.untitled=Unavngivet session diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties index f604c18c85..47248c351f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code ist ein KI-Coding-Assistent. Bitten Sie ihn, Fun session.empty.loading=Wird geladen… session.empty.recent=ZULETZT session.showHistory=Verlauf anzeigen +feedback.button=Feedback & Support +feedback.dialog.message=Wir würden uns freuen, Ihr Feedback zu hören oder Ihnen bei Problemen zu helfen. +feedback.dialog.github=Ein Problem auf GitHub melden +feedback.dialog.discord=Unserer Discord-Community beitreten +feedback.dialog.support=Kundensupport session.scroll.bottom=Zum Ende scrollen session.tab.new=Neue Sitzung session.tab.untitled=Unbenannte Sitzung diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties index 92fbad3508..ce41888338 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code es un asistente de codificación con IA. Pida qu session.empty.loading=Cargando… session.empty.recent=RECIENTE session.showHistory=Mostrar historial +feedback.button=Comentarios y soporte +feedback.dialog.message=Nos encantaría escuchar tus comentarios o ayudarte con cualquier problema que estés experimentando. +feedback.dialog.github=Reportar un problema en GitHub +feedback.dialog.discord=Unirse a nuestra comunidad de Discord +feedback.dialog.support=Atención al cliente session.scroll.bottom=Desplazarse al final session.tab.new=Nueva sesión session.tab.untitled=Sesión sin título diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties index bf51dd4e42..27bc8f2a24 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code est un assistant de codage IA. Demandez-lui de c session.empty.loading=Chargement… session.empty.recent=RÉCENT session.showHistory=Afficher l'historique +feedback.button=Commentaires & support +feedback.dialog.message=Nous aimerions recueillir vos commentaires ou vous aider avec les problèmes que vous rencontrez. +feedback.dialog.github=Signaler un problème sur GitHub +feedback.dialog.discord=Rejoindre notre communauté Discord +feedback.dialog.support=Service client session.scroll.bottom=Faire défiler vers le bas session.tab.new=Nouvelle session session.tab.untitled=Session sans titre diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties index c32cc633e9..8d750daf83 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo CodeはAIコーディングアシスタントです session.empty.loading=読み込み中… session.empty.recent=最近 session.showHistory=履歴を表示 +feedback.button=フィードバック & サポート +feedback.dialog.message=フィードバックをお聞かせいただくか、問題がある場合はお気軽にご相談ください。 +feedback.dialog.github=GitHubで問題を報告する +feedback.dialog.discord=Discordコミュニティに参加する +feedback.dialog.support=カスタマーサポート session.scroll.bottom=一番下にスクロール session.tab.new=新しいセッション session.tab.untitled=名前なしのセッション diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties index 893abc95b9..68564e91ac 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code는 AI 코딩 어시스턴트입니다. 기능 session.empty.loading=로딩 중… session.empty.recent=최근 session.showHistory=기록 보기 +feedback.button=피드백 & 지원 +feedback.dialog.message=피드백을 들려주시거나 겪고 계신 문제에 대해 도움을 드리고 싶습니다. +feedback.dialog.github=GitHub에 이슈 보고하기 +feedback.dialog.discord=Discord 커뮤니티 참여하기 +feedback.dialog.support=고객 지원 session.scroll.bottom=맨 아래로 스크롤 session.tab.new=새 세션 session.tab.untitled=제목 없는 세션 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties index e88d796099..e7170dd046 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code is een AI-codeerassistent. Vraag het om functies session.empty.loading=Laden… session.empty.recent=RECENT session.showHistory=Geschiedenis weergeven +feedback.button=Feedback & Ondersteuning +feedback.dialog.message=We horen graag uw feedback of helpen met eventuele problemen die u ervaart. +feedback.dialog.github=Meld een probleem op GitHub +feedback.dialog.discord=Word lid van onze Discord community +feedback.dialog.support=Klantenservice session.scroll.bottom=Naar beneden scrollen session.tab.new=Nieuwe sessie session.tab.untitled=Naamloze sessie diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties index e7631a21d2..efef395431 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code er en AI-kodingsassistent. Be den om å bygge fu session.empty.loading=Laster… session.empty.recent=NYLIGE session.showHistory=Vis historikk +feedback.button=Tilbakemelding & støtte +feedback.dialog.message=Vi vil gjerne høre tilbakemeldingene dine eller hjelpe med problemer du opplever. +feedback.dialog.github=Rapporter et problem på GitHub +feedback.dialog.discord=Bli med i Discord-fellesskapet vårt +feedback.dialog.support=Kundestøtte session.scroll.bottom=Rull til bunnen session.tab.new=Ny økt session.tab.untitled=Uten tittel diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties index 53ddeb68b1..19efd536fe 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code to asystent kodowania AI. Poproś go o tworzenie session.empty.loading=Ładowanie… session.empty.recent=OSTATNIE session.showHistory=Pokaż historię +feedback.button=Opinie i wsparcie +feedback.dialog.message=Chętnie poznamy Twoją opinię lub pomożemy w przypadku problemów. +feedback.dialog.github=Zgłoś problem na GitHubie +feedback.dialog.discord=Dołącz do naszej społeczności Discord +feedback.dialog.support=Wsparcie klienta session.scroll.bottom=Przewiń na dół session.tab.new=Nowa sesja session.tab.untitled=Sesja bez tytułu diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties index 27c7eb4843..eb91302e64 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code é um assistente de codificação com IA. Peça session.empty.loading=Carregando… session.empty.recent=RECENTE session.showHistory=Mostrar histórico +feedback.button=Feedback e suporte +feedback.dialog.message=Adoraríamos ouvir seu feedback ou ajudar com quaisquer problemas que você esteja enfrentando. +feedback.dialog.github=Reportar um problema no GitHub +feedback.dialog.discord=Entrar na nossa comunidade Discord +feedback.dialog.support=Suporte ao cliente session.scroll.bottom=Rolar para o fim session.tab.new=Nova sessão session.tab.untitled=Sessão sem título diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties index e6bdb4d1c6..f7f0f4f0a7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code — это AI-ассистент по прогр session.empty.loading=Загрузка… session.empty.recent=НЕДАВНИЕ session.showHistory=Показать историю +feedback.button=Отзывы и поддержка +feedback.dialog.message=Мы будем рады услышать ваши отзывы или помочь с любыми возникающими проблемами. +feedback.dialog.github=Сообщить о проблеме на GitHub +feedback.dialog.discord=Присоединиться к нашему Discord +feedback.dialog.support=Служба поддержки session.scroll.bottom=Прокрутить вниз session.tab.new=Новая сессия session.tab.untitled=Незаголовок сессия diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties index 049b1e1d1a..d886db273e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code คือผู้ช่วยเขียนโ session.empty.loading=กำลังโหลด… session.empty.recent=ล่าสุด session.showHistory=แสดงประวัติ +feedback.button=ข้อเสนอแนะและการสนับสนุน +feedback.dialog.message=เรายินดีรับฟังข้อเสนอแนะของคุณหรือช่วยแก้ไขปัญหาที่คุณพบ +feedback.dialog.github=รายงานปัญหาบน GitHub +feedback.dialog.discord=เข้าร่วมชุมชน Discord ของเรา +feedback.dialog.support=ฝ่ายสนับสนุนลูกค้า session.scroll.bottom=เลื่อนไปด้านล่าง session.tab.new=เซสชันใหม่ session.tab.untitled=เซสชันไม่มีชื่อ diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties index 096fcb1ee9..b10fe52716 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code, bir yapay zeka kodlama asistanıdır. Özellik session.empty.loading=Yükleniyor… session.empty.recent=SON session.showHistory=Geçmişi göster +feedback.button=Geri Bildirim ve Destek +feedback.dialog.message=Geri bildiriminizi almaktan veya yaşadığınız sorunlarda yardımcı olmaktan mutluluk duyarız. +feedback.dialog.github=GitHub'da sorun bildirin +feedback.dialog.discord=Discord topluluğumuza katılın +feedback.dialog.support=Müşteri Desteği session.scroll.bottom=En alta kaýr session.tab.new=Yeni oturum session.tab.untitled=Başlıksız oturum diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties index 73d770b3c0..6d870a273f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code — це AI-асистент для програ session.empty.loading=Завантаження… session.empty.recent=НЕДАВНІ session.showHistory=Показати історію +feedback.button=Зворотний зв'язок і підтримка +feedback.dialog.message=Ми раді отримати ваш відгук або допомогти з будь-якими проблемами, які у вас виникли. +feedback.dialog.github=Повідомити про проблему на GitHub +feedback.dialog.discord=Приєднатися до нашої спільноти Discord +feedback.dialog.support=Служба підтримки клієнтів session.scroll.bottom=Прокрутити донизу session.tab.new=Нова сесія session.tab.untitled=Сесія без назви diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties index 2288060553..d7f9efc132 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code 是一个 AI 编程助手。可请它构建功 session.empty.loading=加载中… session.empty.recent=最近 session.showHistory=显示历史 +feedback.button=反馈与支持 +feedback.dialog.message=我们很乐意听取您的反馈,或帮助解决您遇到的任何问题。 +feedback.dialog.github=在 GitHub 上报告问题 +feedback.dialog.discord=加入我们的 Discord 社区 +feedback.dialog.support=客户支持 session.scroll.bottom=滚动到底部 session.tab.new=新建会话 session.tab.untitled=无标题会话 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties index 40fb78179b..ae919bc03d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties @@ -9,6 +9,11 @@ session.empty.welcome=Kilo Code 是 AI 程式輔助。可請它建置功能、 session.empty.loading=載入中… session.empty.recent=最近 session.showHistory=顯示歷史 +feedback.button=意見回饋與支援 +feedback.dialog.message=我們很樂意聆聽您的意見回饋,或協助解決您遇到的任何問題。 +feedback.dialog.github=在 GitHub 上回報問題 +feedback.dialog.discord=加入我們的 Discord 社群 +feedback.dialog.support=客戶支援 session.scroll.bottom=滾動到底部 session.tab.new=新建工作階段 session.tab.untitled=未命名的工作階段 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt index 52fb937892..30160c71c5 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt @@ -72,7 +72,7 @@ class SessionUiFactoryTest : BasePlatformTestCase() { val rpc = session("ses_1") val ui = SessionUi(project, workspace, sessions, app, scope, manager = manager, workspaces = workspaces) val controller = controller(ui) - val panel = ai.kilocode.client.session.ui.EmptySessionPanel(testRootDisposable, controller, listOf(rpc)) + val panel = ai.kilocode.client.session.ui.empty.EmptySessionPanel(testRootDisposable, controller, listOf(rpc)) panel.clickRecent(0) @@ -84,7 +84,7 @@ class SessionUiFactoryTest : BasePlatformTestCase() { val manager = FakeManager() val ui = SessionUi(project, workspace, sessions, app, scope, manager = manager, workspaces = workspaces) val controller = controller(ui) - val panel = ai.kilocode.client.session.ui.EmptySessionPanel( + val panel = ai.kilocode.client.session.ui.empty.EmptySessionPanel( testRootDisposable, controller, emptyList(), diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt index 59f5aeb3cd..fc91e303e1 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt @@ -8,7 +8,7 @@ import ai.kilocode.client.session.model.QuestionItem import ai.kilocode.client.session.model.QuestionOption import ai.kilocode.client.session.model.SessionState import ai.kilocode.client.session.ui.ConnectionPanel -import ai.kilocode.client.session.ui.EmptySessionPanel +import ai.kilocode.client.session.ui.empty.EmptySessionPanel import ai.kilocode.client.session.ui.LoadingPanel import ai.kilocode.client.session.ui.prompt.PromptPanel import ai.kilocode.client.session.ui.account.SessionAccountOverlay diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt index 521e678a67..d0c6f0b0ea 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt @@ -6,11 +6,11 @@ import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionActivityKind -import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.history.HistoryTime import ai.kilocode.client.session.history.LocalHistoryItem -import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.controller.SessionController +import ai.kilocode.client.session.ui.empty.EmptySessionPanel +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.ui.FilledBadgeIcon import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi @@ -33,6 +33,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import java.awt.BorderLayout import java.awt.Cursor +import javax.swing.JButton @Suppress("UnstableApiUsage") class EmptySessionPanelTest : BasePlatformTestCase() { @@ -82,10 +83,10 @@ class EmptySessionPanelTest : BasePlatformTestCase() { assertFalse(panel.loadingVisible()) } - fun `test recent section remains visible when empty`() { + fun `test recent section is hidden when empty`() { val panel = panel() - assertTrue(panel.recentVisible()) + assertFalse(panel.recentVisible()) assertEquals(0, panel.recentCount()) } @@ -160,12 +161,21 @@ class EmptySessionPanelTest : BasePlatformTestCase() { assertEquals(ai.kilocode.client.plugin.KiloBundle.message("session.showHistory"), panel.showHistoryText()) } + fun `test feedback button uses localized text and icon`() { + val panel = panel() + + assertEquals(KiloBundle.message("feedback.button"), panel.feedbackText()) + assertNotNull(panel.feedbackIcon()) + } + fun `test action controls use hand cursor and no show history outline`() { val panel = panel() assertFalse(panel.showHistoryBorderPainted()) + assertFalse(panel.feedbackBorderPainted()) assertEquals(Cursor.HAND_CURSOR, panel.showHistoryCursor()) - assertEquals(Cursor.HAND_CURSOR, panel.recentCursor()) + assertEquals(Cursor.HAND_CURSOR, panel.feedbackCursor()) + assertEquals(Cursor.HAND_CURSOR, panel.recent.list.cursor.type) } fun `test clicking show history delegates callback`() { @@ -177,6 +187,36 @@ class EmptySessionPanelTest : BasePlatformTestCase() { assertEquals(1, calls) } + fun `test feedback popup content opens expected destinations`() { + val panel = panel() + val opened = mutableListOf() + val content = panel.feedbackContent { opened.add(it) } + val buttons = UIUtil.uiTraverser(content).filter(JButton::class.java).toList() + + assertEquals( + listOf( + KiloBundle.message("feedback.dialog.github"), + KiloBundle.message("feedback.dialog.discord"), + KiloBundle.message("feedback.dialog.support"), + ), + buttons.map { it.text }, + ) + + buttons.forEach { it.doClick() } + + assertEquals(panel.feedbackUrls(), opened) + } + + fun `test feedback discord action has icon`() { + val panel = panel() + val content = panel.feedbackContent() + val discord = UIUtil.uiTraverser(content) + .filter(JButton::class.java) + .first { it.text == KiloBundle.message("feedback.dialog.discord") } + + assertNotNull(discord.icon) + } + fun `test renderer aligns title center and time east`() { val cell = panel().rendererComponent(session("ses_1")) as BorderLayoutPanel val layout = cell.layout as BorderLayout