From 6b8c736dc1c97544467f6edf8026d271149e4164 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Tue, 21 Jul 2026 14:14:06 -0600 Subject: [PATCH 1/4] feat(cli): add privacy_mode for blurring PII in the TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `privacy_mode` config flag that masks personal/team information in the always-visible sidebar footer and requires explicit confirmation before `/profile` reveals the full account details. The CLI `kilo profile` command is unaffected. Always-visible sidebar: - Personal/team balance renders as `•••` when the flag is on - Team label collapses to "Team credits" instead of the org name - Kilo Pass period usage, bonus, and renew date are hidden `/profile` gate: - Show a DialogConfirm (default: Cancel, action: Reveal) that warns email, name, balance, and team will be exposed before fetching the profile `/privacy` command: - Toggles `privacy_mode` in the global config and refreshes sync Mechanism: - New top-level `privacy_mode` boolean in ConfigV1.Info (kilocode_change) - Registered in the overlay field paths so it's editable - SDK regenerated for the new SdkConfig field - DialogConfirm extended with optional `confirmLabel` and `defaultOption` props (kilocode_change) for the gate UX All edits are isolated to kilocode paths or wrapped in kilocode_change markers to keep the upstream diff minimal. --- .changeset/privacy-mode-tui.md | 5 +++ packages/core/src/v1/config/config.ts | 4 ++ .../opencode/src/kilocode/config/overlay.ts | 1 + .../opencode/src/kilocode/kilo-commands.tsx | 44 +++++++++++++++++++ packages/opencode/src/kilocode/pii.ts | 1 + .../src/kilocode/plugins/sidebar-footer.tsx | 13 ++++-- packages/sdk/js/src/v2/gen/types.gen.ts | 1 + packages/sdk/openapi.json | 3 ++ 8 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 .changeset/privacy-mode-tui.md create mode 100644 packages/opencode/src/kilocode/pii.ts diff --git a/.changeset/privacy-mode-tui.md b/.changeset/privacy-mode-tui.md new file mode 100644 index 0000000000..9131915a7e --- /dev/null +++ b/.changeset/privacy-mode-tui.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Add a privacy mode that blurs PII in the TUI (personal balance, Kilo Pass usage, etc.) and requires confirmation before `/profile` reveals email, name, balance, and team. Toggle with the new `/privacy` command or by setting `privacy_mode` in `kilo.json`. The `kilo profile` CLI command is unaffected. \ No newline at end of file diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index 233f623837..a2781b892f 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -129,6 +129,10 @@ export const Info = Schema.Struct({ hide_prompt_training_models: Schema.optional(Schema.Boolean).annotate({ description: "Hide Kilo Gateway models that may train on your prompts from model listings", }), + privacy_mode: Schema.optional(Schema.Boolean).annotate({ + description: + "Blur personally identifiable information (account email, balance, team name, etc.) in the TUI and require confirmation before showing profile details", + }), sandbox: Schema.optional( Schema.Struct({ enabled: Schema.optional( diff --git a/packages/opencode/src/kilocode/config/overlay.ts b/packages/opencode/src/kilocode/config/overlay.ts index 2d576c3bce..486b7b3abe 100644 --- a/packages/opencode/src/kilocode/config/overlay.ts +++ b/packages/opencode/src/kilocode/config/overlay.ts @@ -80,6 +80,7 @@ export namespace KilocodeConfigOverlay { ["model"], ["small_model"], ["hide_prompt_training_models"], + ["privacy_mode"], ["default_agent"], ["snapshot"], ["share"], diff --git a/packages/opencode/src/kilocode/kilo-commands.tsx b/packages/opencode/src/kilocode/kilo-commands.tsx index 58bb3e8de2..b8ffd08147 100644 --- a/packages/opencode/src/kilocode/kilo-commands.tsx +++ b/packages/opencode/src/kilocode/kilo-commands.tsx @@ -11,6 +11,8 @@ import { useRoute } from "@tui/context/route" import { useDialog } from "@tui/ui/dialog" import { useToast } from "@tui/ui/toast" import { DialogAlert } from "@tui/ui/dialog-alert" +import { DialogConfirm } from "@tui/ui/dialog-confirm" +import { reconcile } from "solid-js/store" import type { Organization } from "@kilocode/kilo-gateway" import type { ClawStatus } from "./claw/types.js" import { DialogKiloTeamSelect } from "./components/dialog-kilo-team-select.js" @@ -137,6 +139,15 @@ export function registerKiloCommands(useSDK: () => UseSDK) { hidden: !isKiloConnected(), run: async () => { try { + if (sync.data.config.privacy_mode === true) { + const confirmed = await DialogConfirm.show( + dialog, + "Privacy Mode Enabled", + "Privacy mode is on. Revealing your profile will display your email, name, balance, and team on screen.", + ) + if (confirmed !== true) return + } + // Fetch profile and balance using server endpoint const response = await sdk.client.kilo.profile() @@ -176,6 +187,39 @@ export function registerKiloCommands(useSDK: () => UseSDK) { ] : []), + // /privacy command + { + name: "kilo.privacy", + get title() { + return sync.data.config.privacy_mode === true ? "Disable privacy mode" : "Enable privacy mode" + }, + desc: "Blur PII (balance, email, etc.) and confirm before showing profile", + category: "Kilo", + slashName: "privacy", + run: async () => { + const next = sync.data.config.privacy_mode !== true + const response = await sdk.client.config.overlayUpdate({ + scope: "global", + set: { privacy_mode: next }, + }) + if (response.error) { + const status = response.response?.status ?? "?" + toast.show({ message: `Failed to update privacy mode (${status})`, variant: "error" }) + return + } + const [cfg, global] = await Promise.all([ + sdk.client.config.get({}), + sdk.client.global.config.get({}), + ]) + if (cfg.data) sync.set("config", reconcile(cfg.data)) + if (global.data) sync.set("globalConfig", reconcile(global.data)) + toast.show({ + message: next ? "Privacy mode enabled" : "Privacy mode disabled", + variant: "success", + }) + }, + }, + // /teams command { name: "kilo.teams", diff --git a/packages/opencode/src/kilocode/pii.ts b/packages/opencode/src/kilocode/pii.ts new file mode 100644 index 0000000000..f3d9002156 --- /dev/null +++ b/packages/opencode/src/kilocode/pii.ts @@ -0,0 +1 @@ +export const REDACTED_BALANCE = "•••" diff --git a/packages/opencode/src/kilocode/plugins/sidebar-footer.tsx b/packages/opencode/src/kilocode/plugins/sidebar-footer.tsx index cf605c78bf..b7113eb5b8 100644 --- a/packages/opencode/src/kilocode/plugins/sidebar-footer.tsx +++ b/packages/opencode/src/kilocode/plugins/sidebar-footer.tsx @@ -5,6 +5,7 @@ import * as Log from "@opencode-ai/core/util/log" import type { KiloPassState } from "@kilocode/kilo-gateway" import type { Message } from "@kilocode/sdk/v2" import { onBalanceRefresh } from "../balance-refresh" +import { REDACTED_BALANCE } from "../pii" const id = "internal:kilo-sidebar-footer" const TEAM_POLL_MS = 5 * 60_000 @@ -37,8 +38,9 @@ export function scope(org: string | null | undefined, list?: readonly { id: stri } } -export function creditLabel(value: ReturnType) { +export function creditLabel(value: ReturnType, masked = false) { if (value.kind === "Personal") return "Personal credits" + if (masked) return "Team credits" return value.name ? `${value.name} team` : "Team credits" } @@ -98,6 +100,8 @@ function View(props: { api: TuiPluginApi }) { name: list.at(-1) ?? "", } }) + const privacyMode = createMemo(() => props.api.state.config.privacy_mode === true) + const balanceText = createMemo(() => (privacyMode() ? REDACTED_BALANCE : null)) const refresh = () => { const id = ++seq // Cancel any prior request and time this one out — the client path has no fetch timeout, @@ -167,19 +171,20 @@ function View(props: { api: TuiPluginApi }) { {(() => { const balance = data().balance if (balance === undefined) return null + const masked = balanceText() return ( - {creditLabel(data().scope)} + {creditLabel(data().scope, privacyMode())} - {format(balance)} + {masked ?? format(balance)} ) })()} - + {(pass) => ( diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index fe259e24a0..39f3b7453a 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1582,6 +1582,7 @@ export type Config = { terminal_command_display?: "expanded" | "collapsed" code_edit_display?: "expanded" | "collapsed" hide_prompt_training_models?: boolean + privacy_mode?: boolean /** * Sandbox configuration for agent tools */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index ed1b1a4fd6..65ff805e56 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -28115,6 +28115,9 @@ "hide_prompt_training_models": { "type": "boolean" }, + "privacy_mode": { + "type": "boolean" + }, "sandbox": { "type": "object", "properties": { From 897f48a0ec7c67161f41f831952ac75ff36a0c04 Mon Sep 17 00:00:00 2001 From: Hardik Sharma Date: Fri, 31 Jul 2026 16:25:49 +0530 Subject: [PATCH 2/4] feat(jetbrains): show filenames first in file mentions --- .../session/ui/prompt/KiloPromptCompletionProvider.kt | 2 +- .../ui/prompt/KiloPromptCompletionProviderTest.kt | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt index a2b6331bbb..d028a83028 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt @@ -252,7 +252,7 @@ class KiloPromptCompletionProvider( PrioritizedLookupElement.withGrouping(PrioritizedLookupElement.withPriority(element, 100.0), 100) private fun file(file: WorkspaceFileDto): LookupElement = LookupElementBuilder.create(file.path) - .withPresentableText("@${file.path}") + .withPresentableText("@${file.name}") .withTailText(parent(file.path), true) .withIcon(icon(file)) .withLookupString(file.name) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt index 76077daeab..16b65019b9 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt @@ -211,6 +211,16 @@ class KiloPromptCompletionProviderTest : BasePlatformTestCase() { assertSame(AllIcons.Nodes.Folder, icon("src")) } + fun `test mention completion renders filename before parent path`() { + rpc.searchResult = FileSearchResultDto(files = listOf(file("src/foo/Bar.kt"))) + + complete("@bar") + + val view = LookupElementPresentation().also { item("src/foo/Bar.kt").renderElement(it) } + assertEquals("@Bar.kt", view.itemText) + assertEquals(" src/foo", view.tailText) + } + fun `test highlights known slash command at start`() { assertEquals( listOf(KiloPromptCompletionProvider.Highlight(0, 4, KiloPromptCompletionProvider.HighlightKind.COMMAND)), From 27fd873d9aed51225e6da3de2b7a5ee7ce7c47b8 Mon Sep 17 00:00:00 2001 From: Hardik Sharma Date: Fri, 31 Jul 2026 19:26:27 +0530 Subject: [PATCH 3/4] docs(changeset): note JetBrains file suggestion order --- .changeset/jetbrains-file-suggestions.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/jetbrains-file-suggestions.md diff --git a/.changeset/jetbrains-file-suggestions.md b/.changeset/jetbrains-file-suggestions.md new file mode 100644 index 0000000000..9ac86fcbf9 --- /dev/null +++ b/.changeset/jetbrains-file-suggestions.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show file names before their containing folders in JetBrains `@file` suggestions. From 76f8967dc19a5f4f48726ad401fbd3142fd17750 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Wed, 5 Aug 2026 07:31:50 -0600 Subject: [PATCH 4/4] fix(cli): read privacy_mode only from global config and use balance-independent color The /privacy command wrote only to global config but read the effective config, so a project-level privacy_mode could shadow the global toggle and the UI would not change even though the command reported success. Since privacy mode is a personal preference, persist and read it only from the global config. The sidebar footer used tone() for both the bullet and masked balance, so the bullet color still revealed whether the balance was low. Use theme().textMuted for both while masked, retaining tone() only when privacy mode is off. --- packages/opencode/src/kilocode/kilo-commands.tsx | 6 +++--- packages/opencode/src/kilocode/plugins/sidebar-footer.tsx | 7 ++++--- packages/opencode/test/fixture/tui-plugin.ts | 6 ++++++ packages/plugin/src/tui.ts | 1 + packages/tui/src/plugin/adapters.tsx | 5 +++++ 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/kilocode/kilo-commands.tsx b/packages/opencode/src/kilocode/kilo-commands.tsx index b8ffd08147..5ad85cf974 100644 --- a/packages/opencode/src/kilocode/kilo-commands.tsx +++ b/packages/opencode/src/kilocode/kilo-commands.tsx @@ -139,7 +139,7 @@ export function registerKiloCommands(useSDK: () => UseSDK) { hidden: !isKiloConnected(), run: async () => { try { - if (sync.data.config.privacy_mode === true) { + if (sync.data.globalConfig.privacy_mode === true) { const confirmed = await DialogConfirm.show( dialog, "Privacy Mode Enabled", @@ -191,13 +191,13 @@ export function registerKiloCommands(useSDK: () => UseSDK) { { name: "kilo.privacy", get title() { - return sync.data.config.privacy_mode === true ? "Disable privacy mode" : "Enable privacy mode" + return sync.data.globalConfig.privacy_mode === true ? "Disable privacy mode" : "Enable privacy mode" }, desc: "Blur PII (balance, email, etc.) and confirm before showing profile", category: "Kilo", slashName: "privacy", run: async () => { - const next = sync.data.config.privacy_mode !== true + const next = sync.data.globalConfig.privacy_mode !== true const response = await sdk.client.config.overlayUpdate({ scope: "global", set: { privacy_mode: next }, diff --git a/packages/opencode/src/kilocode/plugins/sidebar-footer.tsx b/packages/opencode/src/kilocode/plugins/sidebar-footer.tsx index b7113eb5b8..994a1b47a1 100644 --- a/packages/opencode/src/kilocode/plugins/sidebar-footer.tsx +++ b/packages/opencode/src/kilocode/plugins/sidebar-footer.tsx @@ -100,8 +100,9 @@ function View(props: { api: TuiPluginApi }) { name: list.at(-1) ?? "", } }) - const privacyMode = createMemo(() => props.api.state.config.privacy_mode === true) + const privacyMode = createMemo(() => props.api.state.globalConfig.privacy_mode === true) const balanceText = createMemo(() => (privacyMode() ? REDACTED_BALANCE : null)) + const mutedColor = createMemo(() => (privacyMode() ? theme().textMuted : tone())) const refresh = () => { const id = ++seq // Cancel any prior request and time this one out — the client path has no fetch timeout, @@ -175,12 +176,12 @@ function View(props: { api: TuiPluginApi }) { return ( - + {creditLabel(data().scope, privacyMode())} - {masked ?? format(balance)} + {masked ?? format(balance)} ) })()} diff --git a/packages/opencode/test/fixture/tui-plugin.ts b/packages/opencode/test/fixture/tui-plugin.ts index 06706bc96b..13860c0d0b 100644 --- a/packages/opencode/test/fixture/tui-plugin.ts +++ b/packages/opencode/test/fixture/tui-plugin.ts @@ -97,6 +97,7 @@ type Opts = { state?: { ready?: HostPluginApi["state"]["ready"] config?: HostPluginApi["state"]["config"] + globalConfig?: HostPluginApi["state"]["globalConfig"] // kilocode_change provider?: HostPluginApi["state"]["provider"] path?: HostPluginApi["state"]["path"] vcs?: HostPluginApi["state"]["vcs"] @@ -303,6 +304,11 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi { get config() { return opts.state?.config ?? {} }, + // kilocode_change start + get globalConfig() { + return opts.state?.globalConfig ?? {} + }, + // kilocode_change end get provider() { return opts.state?.provider ?? [] }, diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 5af4d6ee60..d7b0a709f0 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -376,6 +376,7 @@ export type TuiKV = { export type TuiState = { readonly ready: boolean readonly config: SdkConfig + readonly globalConfig: SdkConfig readonly provider: ReadonlyArray readonly path: { state: string diff --git a/packages/tui/src/plugin/adapters.tsx b/packages/tui/src/plugin/adapters.tsx index 7f0e66c5e7..54cea5055e 100644 --- a/packages/tui/src/plugin/adapters.tsx +++ b/packages/tui/src/plugin/adapters.tsx @@ -107,6 +107,11 @@ function stateApi(sync: ReturnType): TuiPluginApi["state"] { get config() { return sync.data.config }, + // kilocode_change start + get globalConfig() { + return sync.data.globalConfig + }, + // kilocode_change end get provider() { return sync.data.provider },