feat(jetbrains): hint worktree usage on empty session

Show a branch-aware tip under the logo on the empty session screen. On a
plain checkout it nudges toward running the task in a worktree, with
"run it in a worktree" as an inline link that opens the New Worktree
flow. In a worktree it confirms the work is isolated instead.

The tip falls back to the generic welcome whenever a claim would be
wrong: before branch status resolves, when git is missing, and on a
detached HEAD. Read-only surfaces show no tip and skip the fetch.

Branch status now feeds the empty panel as well as the branch dock, so
worktree editor tabs get it too even though they render no dock.

Translate the new strings into all 18 locales. The tip keys carry
placeholders and therefore go through MessageFormat, where a lone
apostrophe silently swallows surrounding text, so KiloBundleLocaleTest
formats every locale for real and checks apostrophe escaping.
This commit is contained in:
kirillk
2026-08-27 13:58:15 -04:00
parent d96b1b3c3d
commit a453e2d231
26 changed files with 492 additions and 13 deletions
@@ -101,6 +101,7 @@ abstract class SessionHost(
activity = { activity() },
titles = { titles() },
timers = timers,
newWorktree = if (supportsNewWorktree) ({ newWorktree() }) else null,
)
@RequiresEdt
@@ -52,6 +52,7 @@ interface SessionManager {
history = { showHistory() },
activity = { activity() },
titles = { titles() },
newWorktree = if (supportsNewWorktree) ({ newWorktree() }) else null,
)
fun openSession(session: SessionDto) {
@@ -72,6 +72,7 @@ import ai.kilocode.client.util.UiTimerSource
import ai.kilocode.client.util.UiTimers
import ai.kilocode.client.vfs.KiloVfsManager
import ai.kilocode.log.ChatLogSummary
import ai.kilocode.rpc.dto.BranchStatusDto
import ai.kilocode.rpc.dto.ModelLimitDto
import ai.kilocode.rpc.dto.DiffFileDto
import ai.kilocode.rpc.dto.PromptDto
@@ -213,6 +214,12 @@ class SessionUi(
private lateinit var load: LoadingPanel
private lateinit var migrationWizard: MigrationWizardPanel
private var empty: EmptySessionPanel? = null
/**
* Last observed branch/worktree status. Retained so an empty panel created after the fetch shows
* its tip immediately instead of waiting for the next refresh.
*/
private var branch: BranchStatusDto? = null
private var modalFocus: (() -> JComponent)? = null
private var style = SessionEditorStyle.current()
private val selection = SessionSelection()
@@ -247,8 +254,8 @@ class SessionUi(
dock?.let {
syncDock()
refreshBranchChanges()
refreshBranch()
}
refreshBranch()
loaded?.let(::finishOpen)
}
@@ -613,6 +620,7 @@ class SessionUi(
val panel = manager?.emptyPanel(this, controller)
?: EmptySessionPanel(this, controller, controller.recents(), timers = timers)
empty = panel
panel.setBranch(branch)
scroll.show(panel.view)
}
@@ -1023,7 +1031,9 @@ class SessionUi(
* split mode — so the PR always matches the branch checked out in this session's directory.
*/
private fun refreshBranch() {
val dock = dock ?: return
// Also feeds the empty panel's branch/worktree tip, so this runs even on surfaces without a
// dock (worktree editor tabs). Read-only surfaces show no tip and get no fetch.
if (readonly) return
branchJob?.cancel()
branchJob = cs.launch {
val status = runCatching { service<KiloWorktreeService>().branchStatus(workspace.directory) }
@@ -1034,7 +1044,9 @@ class SessionUi(
}
withContext(Dispatchers.Main) {
if (disposed || project.isDisposed) return@withContext
dock.setBranch(status)
branch = status
dock?.setBranch(status)
empty?.setBranch(status)
}
}
}
@@ -14,12 +14,16 @@ import ai.kilocode.client.ui.layout.VAlign
import ai.kilocode.client.ui.layout.align
import ai.kilocode.client.util.UiTimerSource
import ai.kilocode.client.util.UiTimers
import ai.kilocode.rpc.dto.BranchStatusDto
import ai.kilocode.rpc.dto.GhAvailability
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.openapi.util.text.HtmlChunk
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.Centerizer
@@ -39,6 +43,8 @@ import java.awt.event.MouseEvent
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.SwingUtilities
import javax.swing.event.HyperlinkEvent
import javax.swing.event.HyperlinkListener
/**
* Empty-session panel.
@@ -56,6 +62,7 @@ class EmptySessionPanel(
private val browse: (String) -> Unit = BrowserUtil::browse,
private val timers: UiTimerSource = UiTimers,
private val minimal: Boolean = false,
private val newWorktree: (() -> Unit)? = null,
) : BorderLayoutPanel(), Disposable, SessionEditorStyleTarget {
private var style = SessionEditorStyle.current()
val view: Align = align(
@@ -71,6 +78,12 @@ class EmptySessionPanel(
addActionListener { history() }
}
/**
* Branch/worktree status behind the tip under the logo. Null until [setBranch] delivers it, so
* the first paint falls back to the generic welcome rather than flashing a wrong claim.
*/
private var branch: BranchStatusDto? = null
private val feedback = EmptySessionFeedback(browse)
private val logo = JBLabel(
@@ -79,10 +92,21 @@ class EmptySessionPanel(
horizontalAlignment = JBLabel.CENTER
}
private val welcomeLabel = JBLabel(welcomeHtml()).apply {
/**
* Text is set by [syncTip], which picks the generic welcome or a branch/worktree tip. Copyable
* mode swaps the label's internals for an HTML pane, which is what makes the inline worktree
* link clickable; auto-wrapping must be set first so that pane's CSS allows line breaks.
*/
private val welcomeLabel = object : JBLabel() {
override fun createHyperlinkListener() = HyperlinkListener { e ->
if (e.eventType != HyperlinkEvent.EventType.ACTIVATED) return@HyperlinkListener
if (e.description == WORKTREE_HREF) newWorktree?.invoke()
}
}.apply {
foreground = SessionUiStyle.Text.Secondary.foreground()
horizontalAlignment = JBLabel.CENTER
setAllowAutoWrapping(true)
setCopyable(true)
}
private val description = object : BorderLayoutPanel() {
@@ -101,6 +125,13 @@ class EmptySessionPanel(
add(welcomeLabel, BorderLayout.CENTER)
}
private val descriptionSlot = description.align(HAlign.CENTER, VAlign.CENTER)
private val header = BorderLayoutPanel(0, UiStyle.Gap.pad()).apply {
isOpaque = false
add(logo, BorderLayout.NORTH)
}
init {
Disposer.register(parent, this)
Disposer.register(this, feedback)
@@ -119,12 +150,6 @@ class EmptySessionPanel(
val gap = UiStyle.Gap.pad()
layout = BorderLayout(0, gap)
val header = BorderLayoutPanel(0, gap).apply {
isOpaque = false
add(logo, BorderLayout.NORTH)
if (!minimal) add(description.align(HAlign.CENTER, VAlign.CENTER), BorderLayout.CENTER)
}
val actions = Stack.vertical(gap = UiStyle.Gap.lg())
if (!minimal) actions.next(Centerizer(historyButton, Centerizer.TYPE.HORIZONTAL))
actions.next(Centerizer(feedback.button, Centerizer.TYPE.HORIZONTAL))
@@ -136,6 +161,68 @@ class EmptySessionPanel(
add(header, BorderLayout.NORTH)
if (!minimal && recent.hasSessions()) add(recent, BorderLayout.CENTER)
add(south, BorderLayout.SOUTH)
syncTip()
}
/**
* Applies the branch/worktree status behind the tip under the logo. Called again whenever the
* status is refreshed, so it must stay a no-op when nothing changed.
*/
@RequiresEdt
fun setBranch(status: BranchStatusDto?) {
if (branch == status) return
branch = status
syncTip()
}
/**
* The tip under the logo, as an HTML fragment: an isolation reminder on a worktree, a nudge
* towards one on a plain checkout, where "run it in a worktree" is an inline link. Null when the
* status is unknown, git is missing, or no branch is checked out — the generic welcome covers
* those rather than asserting something wrong.
*/
private fun tip(): String? {
val status = branch ?: return null
if (status.availability == GhAvailability.GIT_MISSING) return null
val name = name()?.let { XmlStringUtil.escapeString(it) }
if (status.worktree) {
return name?.let { KiloBundle.message("session.empty.worktree", it) }
?: KiloBundle.message("session.empty.worktree.unknown")
}
if (name == null) return null
return KiloBundle.message("session.empty.branch", name, worktreePhrase())
}
/**
* "run it in a worktree" as a link, or as plain text on surfaces that cannot open the flow, so
* the sentence reads the same either way.
*/
private fun worktreePhrase(): String {
val phrase = KiloBundle.message("session.empty.branch.link")
if (newWorktree == null) return XmlStringUtil.escapeString(phrase)
return HtmlChunk.link(WORKTREE_HREF, phrase).toString()
}
/** Branch name trimmed to fit the fixed-width description, or null when there is no branch. */
private fun name(): String? {
val value = branch?.branch?.trim().orEmpty()
if (value.isEmpty() || value == DETACHED) return null
return StringUtil.shortenTextWithEllipsis(value, BRANCH_MAX, 0, true)
}
@RequiresEdt
private fun syncTip() {
val tip = tip()
welcomeLabel.text = centeredHtml(
tip ?: XmlStringUtil.escapeString(KiloBundle.message("session.empty.welcome")),
)
// Minimal surfaces (worktree/subagent editor tabs) skip the generic blurb but still want a
// state-specific tip, so the slot is attached on demand instead of once at construction.
val described = tip != null || !minimal
if (described && descriptionSlot.parent == null) header.add(descriptionSlot, BorderLayout.CENTER)
if (!described && descriptionSlot.parent != null) header.remove(descriptionSlot)
revalidate()
repaint()
}
internal fun recentCount() = recent.count()
@@ -184,6 +271,16 @@ class EmptySessionPanel(
internal fun explanationText() = KiloBundle.message("session.empty.welcome")
/** The tip as the user reads it, with the inline link's markup stripped. */
internal fun tipText() = tip()?.let { StringUtil.removeHtmlTags(it) }
/** The plain text currently under the logo: the state-specific tip, or the generic welcome. */
internal fun descriptionText() = tipText() ?: KiloBundle.message("session.empty.welcome")
internal fun worktreeLinked() = tip()?.contains("href=\"$WORKTREE_HREF\"") == true
internal fun worktreeHref() = WORKTREE_HREF
internal fun welcomeLabelAlignment() = welcomeLabel.horizontalAlignment
internal fun descriptionPreferredSize() = description.preferredSize
@@ -272,11 +369,19 @@ class EmptySessionPanel(
repaint()
}
private fun welcomeHtml() = XmlStringUtil.wrapInHtml(
"<div style='text-align:center'>${XmlStringUtil.escapeString(KiloBundle.message("session.empty.welcome"))}</div>"
/** [body] must already be escaped or generated HTML — this only wraps and centers it. */
private fun centeredHtml(body: String) = XmlStringUtil.wrapInHtml(
"<div style='text-align:center'>$body</div>"
)
private companion object {
const val ACTIVITY_MS = 3_000
/** Keeps a long branch name from wrapping the fixed-width description into a wall of text. */
const val BRANCH_MAX = 28
const val DETACHED = "(detached)"
/** Href of the inline worktree link; matched in the label's hyperlink listener. */
const val WORKTREE_HREF = "worktree"
}
}
@@ -28,6 +28,10 @@ session.connection.warning.config=Configuration warnings
notification.group.kilo=Kilo Code
session.empty.welcome=Kilo Code is an AI coding assistant. Ask it to build features, fix bugs, or explain your codebase.
session.empty.branch=You''re working directly on {0}. Start a task, or {1} to keep changes isolated.
session.empty.branch.link=run it in a worktree
session.empty.worktree=You''re in an isolated worktree on {0}. Work freely — your main checkout stays untouched.
session.empty.worktree.unknown=You're in an isolated worktree. Work freely — your main checkout stays untouched.
session.account.balance=Balance: {0}
session.account.switcher=Switch account
session.empty.loading=Loading...
@@ -17,6 +17,10 @@ session.connection.unsupported.options=الخيار 1: افتح المشروع
session.connection.warning.config=تحذيرات التكوين
session.empty.welcome=Kilo Code هو مساعد برمجة بالذكاء الاصطناعي. اطلب منه بناء ميزات أو إصلاح أخطاء أو شرح قاعدة الكود.
session.empty.branch=تعمل مباشرةً على {0}. ابدأ مهمة، أو {1} لإبقاء التغييرات معزولة.
session.empty.branch.link=شغّلها في worktree
session.empty.worktree=أنت في worktree معزول على {0}. اعمل بحرية — تبقى نسختك الرئيسية دون تغيير.
session.empty.worktree.unknown=أنت في worktree معزول. اعمل بحرية — تبقى نسختك الرئيسية دون تغيير.
session.empty.loading=جاري التحميل…
session.empty.recent=الحديثة
session.showHistory=عرض السجل
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=الإعدادات
action.Kilo.Settings.description=إعدادات Kilo Code
action.Kilo.NewSession.text=جلسة جديدة
action.Kilo.NewSession.description=بدء جلسة Kilo جديدة
action.Kilo.NewSession.toolbar=جلسة
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=السجل
action.Kilo.History.description=عرض سجل الجلسات
action.Kilo.SendPrompt.text=إرسال الطلب
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Opcija 1: Otvorite projekat u kontejneru
session.connection.warning.config=Upozorenja konfiguracije
session.empty.welcome=Kilo Code je AI asistent za kodiranje. Zatražite od njega da gradi funkcije, ispravlja greške ili objašnjava vašu bazu koda.
session.empty.branch=Radite direktno na {0}. Započnite zadatak ili {1} da izmjene ostanu izolovane.
session.empty.branch.link=pokrenite ga u worktree-u
session.empty.worktree=Nalazite se u izolovanom worktree-u na {0}. Radite slobodno — vaš glavni checkout ostaje nepromijenjen.
session.empty.worktree.unknown=Nalazite se u izolovanom worktree-u. Radite slobodno — vaš glavni checkout ostaje nepromijenjen.
session.empty.loading=Učitavanje…
session.empty.recent=NEDAVNO
session.showHistory=Prikaži historiju
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=Postavke
action.Kilo.Settings.description=Postavke Kilo Code
action.Kilo.NewSession.text=Nova sesija
action.Kilo.NewSession.description=Pokrenite novu Kilo sesiju
action.Kilo.NewSession.toolbar=Sesija
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Historija
action.Kilo.History.description=Prikaži historiju sesija
action.Kilo.SendPrompt.text=Pošalji upit
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Mulighed 1: Åbn projektet i containeren
session.connection.warning.config=Konfigurationsadvarsler
session.empty.welcome=Kilo Code er en AI-kodningsassistent. Bed den om at bygge funktioner, rette fejl eller forklare din kodebase.
session.empty.branch=Du arbejder direkte på {0}. Start en opgave, eller {1} for at holde ændringer isoleret.
session.empty.branch.link=kør den i et worktree
session.empty.worktree=Du er i et isoleret worktree på {0}. Arbejd frit — dit primære checkout forbliver urørt.
session.empty.worktree.unknown=Du er i et isoleret worktree. Arbejd frit — dit primære checkout forbliver urørt.
session.empty.loading=Indlæser…
session.empty.recent=SENESTE
session.showHistory=Vis historik
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=Indstillinger
action.Kilo.Settings.description=Kilo Code-indstillinger
action.Kilo.NewSession.text=Ny session
action.Kilo.NewSession.description=Start en ny Kilo-session
action.Kilo.NewSession.toolbar=Session
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Historik
action.Kilo.History.description=Vis sessionshistorik
action.Kilo.SendPrompt.text=Send prompt
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Option 1: Öffnen Sie das Projekt im Cont
session.connection.warning.config=Konfigurationswarnungen
session.empty.welcome=Kilo Code ist ein KI-Coding-Assistent. Bitten Sie ihn, Funktionen zu erstellen, Fehler zu beheben oder Ihre Codebasis zu erklären.
session.empty.branch=Sie arbeiten direkt auf {0}. Starten Sie eine Aufgabe oder {1}, um Änderungen isoliert zu halten.
session.empty.branch.link=führen Sie sie in einem Worktree aus
session.empty.worktree=Sie sind in einem isolierten Worktree auf {0}. Arbeiten Sie frei — Ihr Haupt-Checkout bleibt unberührt.
session.empty.worktree.unknown=Sie sind in einem isolierten Worktree. Arbeiten Sie frei — Ihr Haupt-Checkout bleibt unberührt.
session.empty.loading=Wird geladen…
session.empty.recent=ZULETZT
session.showHistory=Verlauf anzeigen
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=Einstellungen
action.Kilo.Settings.description=Kilo Code Einstellungen
action.Kilo.NewSession.text=Neue Sitzung
action.Kilo.NewSession.description=Neue Kilo-Sitzung starten
action.Kilo.NewSession.toolbar=Sitzung
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Verlauf
action.Kilo.History.description=Sitzungsverlauf anzeigen
action.Kilo.SendPrompt.text=Anfrage senden
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Opción 1: Abre el proyecto en el contene
session.connection.warning.config=Advertencias de configuración
session.empty.welcome=Kilo Code es un asistente de codificación con IA. Pida que construya funciones, corrija errores o explique su base de código.
session.empty.branch=Estás trabajando directamente en {0}. Inicia una tarea o {1} para mantener los cambios aislados.
session.empty.branch.link=ejecútala en un worktree
session.empty.worktree=Estás en un worktree aislado en {0}. Trabaja con libertad — tu checkout principal no se toca.
session.empty.worktree.unknown=Estás en un worktree aislado. Trabaja con libertad — tu checkout principal no se toca.
session.empty.loading=Cargando…
session.empty.recent=RECIENTE
session.showHistory=Mostrar historial
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=Configuración
action.Kilo.Settings.description=Configuración de Kilo Code
action.Kilo.NewSession.text=Nueva sesión
action.Kilo.NewSession.description=Iniciar una nueva sesión de Kilo
action.Kilo.NewSession.toolbar=Sesión
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Historial
action.Kilo.History.description=Mostrar el historial de sesiones
action.Kilo.SendPrompt.text=Enviar mensaje
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Option 1 : ouvrez le projet dans le conte
session.connection.warning.config=Avertissements de configuration
session.empty.welcome=Kilo Code est un assistant de codage IA. Demandez-lui de créer des fonctionnalités, corriger des bugs ou expliquer votre base de code.
session.empty.branch=Vous travaillez directement sur {0}. Lancez une tâche ou {1} pour isoler vos modifications.
session.empty.branch.link=exécutez-la dans un worktree
session.empty.worktree=Vous êtes dans un worktree isolé sur {0}. Travaillez librement — votre checkout principal reste intact.
session.empty.worktree.unknown=Vous êtes dans un worktree isolé. Travaillez librement — votre checkout principal reste intact.
session.empty.loading=Chargement…
session.empty.recent=RÉCENT
session.showHistory=Afficher l'historique
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=Paramètres
action.Kilo.Settings.description=Paramètres de Kilo Code
action.Kilo.NewSession.text=Nouvelle session
action.Kilo.NewSession.description=Démarrer une nouvelle session Kilo
action.Kilo.NewSession.toolbar=Session
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Historique
action.Kilo.History.description=Afficher l'historique des sessions
action.Kilo.SendPrompt.text=Envoyer l'invite
@@ -17,6 +17,10 @@ session.connection.unsupported.options=オプション1: JetBrains Gateway を
session.connection.warning.config=設定の警告
session.empty.welcome=Kilo CodeはAIコーディングアシスタントです。機能の作成、バグの修正、またはコードベースの説明を依頼できます。
session.empty.branch={0} で直接作業しています。タスクを開始するか、{1}すると変更を隔離できます。
session.empty.branch.link=worktree で実行
session.empty.worktree={0} の独立した worktree にいます。メインのチェックアウトには影響しないので自由に作業できます。
session.empty.worktree.unknown=独立した worktree にいます。メインのチェックアウトには影響しないので自由に作業できます。
session.empty.loading=読み込み中…
session.empty.recent=最近
session.showHistory=履歴を表示
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=設定
action.Kilo.Settings.description=Kilo Codeの設定
action.Kilo.NewSession.text=新しいセッション
action.Kilo.NewSession.description=新しいKiloセッションを開始
action.Kilo.NewSession.toolbar=セッション
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=履歴
action.Kilo.History.description=セッション履歴を表示
action.Kilo.SendPrompt.text=プロンプトを送信
@@ -17,6 +17,10 @@ session.connection.unsupported.options=옵션 1: JetBrains Gateway로 컨테이
session.connection.warning.config=구성 경고
session.empty.welcome=Kilo Code는 AI 코딩 어시스턴트입니다. 기능 구축, 버그 수정, 코드베이스 설명을 요청하세요.
session.empty.branch={0}에서 직접 작업하고 있습니다. 작업을 시작하거나 {1}하여 변경 사항을 분리하세요.
session.empty.branch.link=worktree에서 실행
session.empty.worktree={0}의 격리된 worktree에 있습니다. 메인 체크아웃은 영향을 받지 않으니 자유롭게 작업하세요.
session.empty.worktree.unknown=격리된 worktree에 있습니다. 메인 체크아웃은 영향을 받지 않으니 자유롭게 작업하세요.
session.empty.loading=로딩 중…
session.empty.recent=최근
session.showHistory=기록 보기
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=설정
action.Kilo.Settings.description=Kilo Code 설정
action.Kilo.NewSession.text=새 세션
action.Kilo.NewSession.description=새 Kilo 세션 시작
action.Kilo.NewSession.toolbar=세션
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=기록
action.Kilo.History.description=세션 기록 표시
action.Kilo.SendPrompt.text=프롬프트 전송
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Optie 1: Open het project in de container
session.connection.warning.config=Configuratiewaarschuwingen
session.empty.welcome=Kilo Code is een AI-codeerassistent. Vraag het om functies te bouwen, bugs te repareren of uw codebase uit te leggen.
session.empty.branch=Je werkt direct op {0}. Start een taak of {1} om wijzigingen geïsoleerd te houden.
session.empty.branch.link=voer hem uit in een worktree
session.empty.worktree=Je zit in een geïsoleerde worktree op {0}. Werk vrij — je hoofdcheckout blijft ongemoeid.
session.empty.worktree.unknown=Je zit in een geïsoleerde worktree. Werk vrij — je hoofdcheckout blijft ongemoeid.
session.empty.loading=Laden…
session.empty.recent=RECENT
session.showHistory=Geschiedenis weergeven
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=Instellingen
action.Kilo.Settings.description=Kilo Code-instellingen
action.Kilo.NewSession.text=Nieuwe sessie
action.Kilo.NewSession.description=Een nieuwe Kilo-sessie starten
action.Kilo.NewSession.toolbar=Sessie
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Geschiedenis
action.Kilo.History.description=Sessiegeschiedenis weergeven
action.Kilo.SendPrompt.text=Prompt verzenden
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Alternativ 1: Åpne prosjektet i containe
session.connection.warning.config=Konfigurasjonsadvarsler
session.empty.welcome=Kilo Code er en AI-kodingsassistent. Be den om å bygge funksjoner, fikse feil eller forklare kodebasen din.
session.empty.branch=Du jobber direkte på {0}. Start en oppgave, eller {1} for å holde endringene isolert.
session.empty.branch.link=kjør den i et worktree
session.empty.worktree=Du er i et isolert worktree på {0}. Jobb fritt — hovedutsjekkingen din er urørt.
session.empty.worktree.unknown=Du er i et isolert worktree. Jobb fritt — hovedutsjekkingen din er urørt.
session.empty.loading=Laster…
session.empty.recent=NYLIGE
session.showHistory=Vis historikk
@@ -168,6 +172,8 @@ action.Kilo.Settings.text=Innstillinger
action.Kilo.Settings.description=Kilo Code-innstillinger
action.Kilo.NewSession.text=Ny økt
action.Kilo.NewSession.description=Start en ny Kilo-økt
action.Kilo.NewSession.toolbar=Økt
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Historikk
action.Kilo.History.description=Vis økthistorikk
action.Kilo.SendPrompt.text=Send forespørsel
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Opcja 1: Otwórz projekt w kontenerze lub
session.connection.warning.config=Ostrzeżenia konfiguracji
session.empty.welcome=Kilo Code to asystent kodowania AI. Poproś go o tworzenie funkcji, naprawianie błędów lub wyjaśnianie bazy kodu.
session.empty.branch=Pracujesz bezpośrednio na {0}. Rozpocznij zadanie lub {1}, aby odizolować zmiany.
session.empty.branch.link=uruchom je w worktree
session.empty.worktree=Jesteś w odizolowanym worktree na {0}. Pracuj swobodnie — twój główny checkout pozostaje nietknięty.
session.empty.worktree.unknown=Jesteś w odizolowanym worktree. Pracuj swobodnie — twój główny checkout pozostaje nietknięty.
session.empty.loading=Ładowanie…
session.empty.recent=OSTATNIE
session.showHistory=Pokaż historię
@@ -168,6 +172,8 @@ action.Kilo.Settings.text=Ustawienia
action.Kilo.Settings.description=Ustawienia Kilo Code
action.Kilo.NewSession.text=Nowa sesja
action.Kilo.NewSession.description=Rozpocznij nową sesję Kilo
action.Kilo.NewSession.toolbar=Sesja
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Historia
action.Kilo.History.description=Pokaż historię sesji
action.Kilo.SendPrompt.text=Wyślij monit
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Opção 1: Abra o projeto no contêiner o
session.connection.warning.config=Avisos de configuração
session.empty.welcome=Kilo Code é um assistente de codificação com IA. Peça que construa funcionalidades, corrija bugs ou explique sua base de código.
session.empty.branch=Você está trabalhando diretamente em {0}. Inicie uma tarefa ou {1} para manter as alterações isoladas.
session.empty.branch.link=execute-a em um worktree
session.empty.worktree=Você está em um worktree isolado em {0}. Trabalhe livremente — seu checkout principal permanece intacto.
session.empty.worktree.unknown=Você está em um worktree isolado. Trabalhe livremente — seu checkout principal permanece intacto.
session.empty.loading=Carregando…
session.empty.recent=RECENTE
session.showHistory=Mostrar histórico
@@ -168,6 +172,8 @@ action.Kilo.Settings.text=Configurações
action.Kilo.Settings.description=Configurações do Kilo Code
action.Kilo.NewSession.text=Nova sessão
action.Kilo.NewSession.description=Iniciar uma nova sessão do Kilo
action.Kilo.NewSession.toolbar=Sessão
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Histórico
action.Kilo.History.description=Exibir histórico de sessões
action.Kilo.SendPrompt.text=Enviar prompt
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Вариант 1: откройте про
session.connection.warning.config=Предупреждения конфигурации
session.empty.welcome=Kilo Code — это AI-ассистент по программированию. Попросите его разработать функции, исправить ошибки или объяснить кодовую базу.
session.empty.branch=Вы работаете напрямую в {0}. Начните задачу или {1}, чтобы изолировать изменения.
session.empty.branch.link=запустите её в worktree
session.empty.worktree=Вы в изолированном worktree на {0}. Работайте свободно — основная рабочая копия не затрагивается.
session.empty.worktree.unknown=Вы в изолированном worktree. Работайте свободно — основная рабочая копия не затрагивается.
session.empty.loading=Загрузка…
session.empty.recent=НЕДАВНИЕ
session.showHistory=Показать историю
@@ -168,6 +172,8 @@ action.Kilo.Settings.text=Настройки
action.Kilo.Settings.description=Настройки Kilo Code
action.Kilo.NewSession.text=Новая сессия
action.Kilo.NewSession.description=Запустить новую сессию Kilo
action.Kilo.NewSession.toolbar=Сессия
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=История
action.Kilo.History.description=Показать историю сессий
action.Kilo.SendPrompt.text=Отправить запрос
@@ -17,6 +17,10 @@ session.connection.unsupported.options=ตัวเลือกที่ 1: เ
session.connection.warning.config=คำเตือนการกำหนดค่า
session.empty.welcome=Kilo Code คือผู้ช่วยเขียนโค้ด AI ขอให้สร้างฟีเจอร์แก้สันข้อผิดพลาด หรืออธิบายโค้ดเบสของคุณ
session.empty.branch=คุณกำลังทำงานบน {0} โดยตรง เริ่มงานใหม่ หรือ {1} เพื่อแยกการเปลี่ยนแปลงออกจากกัน
session.empty.branch.link=รันใน worktree
session.empty.worktree=คุณอยู่ใน worktree ที่แยกอิสระบน {0} ทำงานได้อย่างอิสระ — เช็คเอาต์หลักของคุณไม่ถูกแตะต้อง
session.empty.worktree.unknown=คุณอยู่ใน worktree ที่แยกอิสระ ทำงานได้อย่างอิสระ — เช็คเอาต์หลักของคุณไม่ถูกแตะต้อง
session.empty.loading=กำลังโหลด…
session.empty.recent=ล่าสุด
session.showHistory=แสดงประวัติ
@@ -168,6 +172,8 @@ action.Kilo.Settings.text=การตั้งค่า
action.Kilo.Settings.description=การตั้งค่า Kilo Code
action.Kilo.NewSession.text=เซสชันใหม่
action.Kilo.NewSession.description=เริ่มเซสชัน Kilo ใหม่
action.Kilo.NewSession.toolbar=เซสชัน
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=ประวัติ
action.Kilo.History.description=แสดงประวัติเซสชัน
action.Kilo.SendPrompt.text=ส่งคำขอ
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Seçenek 1: Projeyi JetBrains Gateway ile
session.connection.warning.config=Yapılandırma uyarıları
session.empty.welcome=Kilo Code, bir yapay zeka kodlama asistanıdır. Özellik oluşturmasını, hata düzeltirlmesi veya kod tabanınızı açıklamasını isteyin.
session.empty.branch=Doğrudan {0} üzerinde çalışıyorsunuz. Bir görev başlatın ya da değişiklikleri izole tutmak için {1}.
session.empty.branch.link=bir worktree içinde çalıştırın
session.empty.worktree={0} üzerinde izole bir worktree içindesiniz. Rahatça çalışın — ana checkout''unuz olduğu gibi kalır.
session.empty.worktree.unknown=İzole bir worktree içindesiniz. Rahatça çalışın — ana checkout'unuz olduğu gibi kalır.
session.empty.loading=Yükleniyor…
session.empty.recent=SON
session.showHistory=Geçmişi göster
@@ -168,6 +172,8 @@ action.Kilo.Settings.text=Ayarlar
action.Kilo.Settings.description=Kilo Code ayarları
action.Kilo.NewSession.text=Yeni oturum
action.Kilo.NewSession.description=Yeni bir Kilo oturumu başlat
action.Kilo.NewSession.toolbar=Oturum
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Geçmiş
action.Kilo.History.description=Oturum geçmişini göster
action.Kilo.SendPrompt.text=İstem gönder
@@ -17,6 +17,10 @@ session.connection.unsupported.options=Варіант 1: відкрийте пр
session.connection.warning.config=Попередження конфігурації
session.empty.welcome=Kilo Code — це AI-асистент для програмування. Попросіть його створити функції, виправити помилки або пояснити ваш код.
session.empty.branch=Ви працюєте безпосередньо в {0}. Почніть завдання або {1}, щоб ізолювати зміни.
session.empty.branch.link=запустіть його в worktree
session.empty.worktree=Ви в ізольованому worktree на {0}. Працюйте вільно — основна робоча копія залишається незмінною.
session.empty.worktree.unknown=Ви в ізольованому worktree. Працюйте вільно — основна робоча копія залишається незмінною.
session.empty.loading=Завантаження…
session.empty.recent=НЕДАВНІ
session.showHistory=Показати історію
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=Налаштування
action.Kilo.Settings.description=Налаштування Kilo Code
action.Kilo.NewSession.text=Нова сесія
action.Kilo.NewSession.description=Розпочати нову сесію Kilo
action.Kilo.NewSession.toolbar=Сесія
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=Історія
action.Kilo.History.description=Показати історію сесій
action.Kilo.SendPrompt.text=Надіслати запит
@@ -17,6 +17,10 @@ session.connection.unsupported.options=选项 1:使用 JetBrains Gateway 在
session.connection.warning.config=配置警告
session.empty.welcome=Kilo Code 是一个 AI 编程助手。可请它构建功能、修复错误或解释您的代码库。
session.empty.branch=你正在 {0} 上直接工作。开始一个任务,或{1}以隔离改动。
session.empty.branch.link=在 worktree 中运行
session.empty.worktree=你在 {0} 的独立 worktree 中。可以放心工作 — 主工作副本不会受到影响。
session.empty.worktree.unknown=你在独立的 worktree 中。可以放心工作 — 主工作副本不会受到影响。
session.empty.loading=加载中…
session.empty.recent=最近
session.showHistory=显示历史
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=设置
action.Kilo.Settings.description=Kilo Code 设置
action.Kilo.NewSession.text=新建会话
action.Kilo.NewSession.description=开始新的 Kilo 会话
action.Kilo.NewSession.toolbar=会话
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=历史
action.Kilo.History.description=显示会话历史
action.Kilo.SendPrompt.text=发送提示
@@ -17,6 +17,10 @@ session.connection.unsupported.options=選項 1:使用 JetBrains Gateway 在
session.connection.warning.config=設定警告
session.empty.welcome=Kilo Code 是 AI 程式輔助。可請它建置功能、修復錯誤或解釋您的程式程式庫。
session.empty.branch=你正在 {0} 上直接工作。開始一個任務,或{1}以隔離變更。
session.empty.branch.link=在 worktree 中執行
session.empty.worktree=你在 {0} 的獨立 worktree 中。可以放心工作 — 主工作副本不會受到影響。
session.empty.worktree.unknown=你在獨立的 worktree 中。可以放心工作 — 主工作副本不會受到影響。
session.empty.loading=載入中…
session.empty.recent=最近
session.showHistory=顯示歷史
@@ -163,6 +167,8 @@ action.Kilo.Settings.text=設定
action.Kilo.Settings.description=Kilo Code 設定
action.Kilo.NewSession.text=新建工作階段
action.Kilo.NewSession.description=開始新的 Kilo 工作階段
action.Kilo.NewSession.toolbar=會話
action.Kilo.NewWorktree.toolbar=Worktree
action.Kilo.History.text=歷史
action.Kilo.History.description=顯示工作階段歷史
action.Kilo.SendPrompt.text=發送提示
@@ -0,0 +1,100 @@
package ai.kilocode.client.plugin
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import java.text.MessageFormat
import java.util.Locale
import java.util.Properties
/**
* Guards the localized empty-session tip and toolbar labels.
*
* The tip keys carry `{0}`/`{1}`, so they go through [MessageFormat], where a lone apostrophe
* silently swallows the surrounding text and `''` collapses to one. Translations are easy to get
* wrong here, so every locale is formatted for real rather than compared as a raw string.
*/
class KiloBundleLocaleTest : BasePlatformTestCase() {
fun `test parameterized tips format cleanly in every locale`() {
for (locale in LOCALES) {
val props = load(locale)
val branch = props.getProperty("session.empty.branch")
assertNotNull("$locale: missing session.empty.branch", branch)
assertEscaped(locale, "session.empty.branch", branch!!)
val rendered = format(branch, "main", "LINK_PHRASE")
assertTrue("$locale: branch tip dropped the branch name -> $rendered", rendered.contains("main"))
assertTrue("$locale: branch tip dropped the link -> $rendered", rendered.contains("LINK_PHRASE"))
assertClean(locale, "session.empty.branch", rendered)
val worktree = props.getProperty("session.empty.worktree")
assertNotNull("$locale: missing session.empty.worktree", worktree)
assertEscaped(locale, "session.empty.worktree", worktree!!)
val tree = format(worktree, "feature/x")
assertTrue("$locale: worktree tip dropped the branch name -> $tree", tree.contains("feature/x"))
assertClean(locale, "session.empty.worktree", tree)
}
}
fun `test plain keys are present and carry no placeholders`() {
for (locale in LOCALES) {
val props = load(locale)
for (key in PLAIN) {
val value = props.getProperty(key)
assertNotNull("$locale: missing $key", value)
assertTrue("$locale: $key is blank", value!!.isNotBlank())
assertFalse("$locale: $key should not contain a placeholder -> $value", value.contains("{0}"))
assertFalse(
"$locale: $key has no placeholders so apostrophes must not be doubled -> $value",
value.contains("''"),
)
}
}
}
private fun format(pattern: String, vararg args: String) =
MessageFormat(pattern, Locale.ROOT).format(args)
/**
* Every apostrophe in a MessageFormat pattern must be doubled. A lone one opens a quoted run
* that silently eats itself (and any placeholder it spans), which formatting alone will not
* always reveal — so the raw pattern is checked directly.
*/
private fun assertEscaped(locale: String, key: String, pattern: String) {
for (run in Regex("'+").findAll(pattern)) {
assertTrue(
"$locale: $key has an unescaped apostrophe, double it -> $pattern",
run.value.length % 2 == 0,
)
}
}
/** After formatting, MessageFormat has consumed its quoting — leftovers mean a bad pattern. */
private fun assertClean(locale: String, key: String, rendered: String) {
assertFalse("$locale: $key still has a doubled apostrophe -> $rendered", rendered.contains("''"))
assertFalse("$locale: $key left an unformatted placeholder -> $rendered", rendered.contains("{"))
}
private fun load(locale: String): Properties {
val name = if (locale == "en") "/messages/KiloBundle.properties" else "/messages/KiloBundle_$locale.properties"
val stream = javaClass.getResourceAsStream(name)
assertNotNull("$locale: $name not on the classpath", stream)
return Properties().apply {
InputStreamReader(stream!!, StandardCharsets.UTF_8).use { load(it) }
}
}
private companion object {
val LOCALES = listOf(
"en", "ar", "bs", "da", "de", "es", "fr", "ja", "ko", "nl",
"no", "pl", "pt_BR", "ru", "th", "tr", "uk", "zh_CN", "zh_TW",
)
val PLAIN = listOf(
"session.empty.branch.link",
"session.empty.worktree.unknown",
"action.Kilo.NewSession.toolbar",
"action.Kilo.NewWorktree.toolbar",
)
}
}
@@ -15,6 +15,8 @@ import ai.kilocode.client.ui.FilledBadgeIcon
import ai.kilocode.client.testing.FakeAppRpcApi
import ai.kilocode.client.testing.FakeSessionRpcApi
import ai.kilocode.client.testing.FakeWorkspaceRpcApi
import ai.kilocode.rpc.dto.BranchStatusDto
import ai.kilocode.rpc.dto.GhAvailability
import ai.kilocode.rpc.dto.KiloAppStateDto
import ai.kilocode.rpc.dto.KiloAppStatusDto
import ai.kilocode.rpc.dto.KiloWorkspaceStateDto
@@ -34,6 +36,8 @@ import kotlinx.coroutines.runBlocking
import java.awt.BorderLayout
import java.awt.Cursor
import javax.swing.JButton
import javax.swing.JEditorPane
import javax.swing.event.HyperlinkEvent
@Suppress("UnstableApiUsage")
class EmptySessionPanelTest : BasePlatformTestCase() {
@@ -159,6 +163,135 @@ class EmptySessionPanelTest : BasePlatformTestCase() {
)
}
// ---- branch/worktree tip under the logo ----
fun `test no tip until branch status arrives`() {
val panel = panel(newWorktree = {})
assertNull(panel.tipText())
assertFalse(panel.worktreeLinked())
assertEquals(panel.explanationText(), panel.descriptionText())
}
fun `test branch promotes running the task in a worktree`() {
val panel = panel(newWorktree = {})
panel.setBranch(BranchStatusDto(branch = "main", worktree = false))
assertEquals(
"You're working directly on main. Start a task, or run it in a worktree to keep changes isolated.",
panel.tipText(),
)
assertTrue(panel.worktreeLinked())
}
fun `test worktree hints isolation and links nothing`() {
val panel = panel(newWorktree = {})
panel.setBranch(BranchStatusDto(branch = "feature/x", worktree = true))
assertEquals(
"You're in an isolated worktree on feature/x. Work freely — your main checkout stays untouched.",
panel.tipText(),
)
assertFalse(panel.worktreeLinked())
}
fun `test worktree without a branch name falls back to the generic worktree tip`() {
val panel = panel()
panel.setBranch(BranchStatusDto(branch = "(detached)", worktree = true))
assertEquals(
"You're in an isolated worktree. Work freely — your main checkout stays untouched.",
panel.tipText(),
)
}
fun `test detached plain checkout keeps the generic welcome`() {
val panel = panel(newWorktree = {})
panel.setBranch(BranchStatusDto(branch = "(detached)", worktree = false))
assertNull(panel.tipText())
assertFalse(panel.worktreeLinked())
assertEquals(panel.explanationText(), panel.descriptionText())
}
fun `test missing git keeps the generic welcome`() {
val panel = panel(newWorktree = {})
panel.setBranch(
BranchStatusDto(branch = "main", worktree = false, availability = GhAvailability.GIT_MISSING),
)
assertNull(panel.tipText())
assertFalse(panel.worktreeLinked())
}
fun `test long branch name is shortened`() {
val panel = panel()
panel.setBranch(BranchStatusDto(branch = "feature/a-very-long-branch-name-that-keeps-going"))
val tip = panel.tipText().orEmpty()
assertTrue(tip, tip.contains(""))
assertFalse(tip, tip.contains("keeps-going"))
}
fun `test minimal surface shows a worktree tip but no generic welcome`() {
val panel = panel(minimal = true)
assertFalse(panel.descriptionVisible())
panel.setBranch(BranchStatusDto(branch = "feature/x", worktree = true))
assertTrue(panel.descriptionVisible())
}
fun `test phrase stays plain text without a callback`() {
val panel = panel()
panel.setBranch(BranchStatusDto(branch = "main", worktree = false))
assertEquals(
"You're working directly on main. Start a task, or run it in a worktree to keep changes isolated.",
panel.tipText(),
)
assertFalse(panel.worktreeLinked())
}
fun `test activating the inline link invokes the callback`() {
var fired = 0
val panel = panel(newWorktree = { fired++ })
panel.setBranch(BranchStatusDto(branch = "main", worktree = false))
activateLink(panel)
assertEquals(1, fired)
}
fun `test activating the inline link ignores other hrefs`() {
var fired = 0
val panel = panel(newWorktree = { fired++ })
panel.setBranch(BranchStatusDto(branch = "main", worktree = false))
activateLink(panel, href = "https://example.test")
assertEquals(0, fired)
}
/**
* Fires the activation through the editor pane that `setCopyable(true)` installs, so the real
* listener wiring is exercised rather than a stand-in.
*/
private fun activateLink(panel: EmptySessionPanel, href: String = panel.worktreeHref()) {
val pane = UIUtil.uiTraverser(panel).filter(JEditorPane::class.java).first()
assertNotNull(pane)
val event = HyperlinkEvent(pane, HyperlinkEvent.EventType.ACTIVATED, null, href)
pane!!.hyperlinkListeners.forEach { it.hyperlinkUpdate(event) }
}
fun `test selecting recent session does not open it`() {
val panel = panel(listOf(session("ses_1"), session("ses_2")))
@@ -373,7 +506,17 @@ class EmptySessionPanelTest : BasePlatformTestCase() {
activity: () -> Map<String, SessionActivityKind> = { sessions.activitySnapshot() },
titles: () -> Map<String, String> = { emptyMap() },
minimal: Boolean = false,
) = EmptySessionPanel(testRootDisposable, controller, recents, history, activity, titles, minimal = minimal)
newWorktree: (() -> Unit)? = null,
) = EmptySessionPanel(
testRootDisposable,
controller,
recents,
history,
activity,
titles,
minimal = minimal,
newWorktree = newWorktree,
)
private fun flush() = runBlocking {
delay(100)