feat(cli): add privacy_mode for blurring PII in the TUI

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.
This commit is contained in:
Aarav Sharma
2026-07-21 14:25:55 -06:00
parent 938919ab72
commit 6b8c736dc1
8 changed files with 68 additions and 4 deletions
+5
View File
@@ -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.
+4
View File
@@ -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(
@@ -80,6 +80,7 @@ export namespace KilocodeConfigOverlay {
["model"],
["small_model"],
["hide_prompt_training_models"],
["privacy_mode"],
["default_agent"],
["snapshot"],
["share"],
@@ -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",
+1
View File
@@ -0,0 +1 @@
export const REDACTED_BALANCE = "•••"
@@ -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<typeof scope>) {
export function creditLabel(value: ReturnType<typeof scope>, 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 (
<box flexDirection="row" justifyContent="space-between">
<box flexDirection="row" gap={1}>
<text fg={tone()}></text>
<text fg={theme().text}>
<b>{creditLabel(data().scope)}</b>
<b>{creditLabel(data().scope, privacyMode())}</b>
</text>
</box>
<text fg={tone()}>{format(balance)}</text>
<text fg={tone()}>{masked ?? format(balance)}</text>
</box>
)
})()}
<Show when={data().scope.kind === "Personal" ? data().pass : null}>
<Show when={privacyMode() ? null : data().scope.kind === "Personal" ? data().pass : null}>
{(pass) => (
<box gap={0}>
<box flexDirection="row" justifyContent="space-between" gap={1}>
+1
View File
@@ -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
*/
+3
View File
@@ -28115,6 +28115,9 @@
"hide_prompt_training_models": {
"type": "boolean"
},
"privacy_mode": {
"type": "boolean"
},
"sandbox": {
"type": "object",
"properties": {