diff --git a/.changeset/privacy-mode-tui.md b/.changeset/privacy-mode-tui.md new file mode 100644 index 00000000000..9131915a7e9 --- /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 77f5d55160d..8b58a7f3da8 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 6785e654ba2..fce5d9dee02 100644 --- a/packages/opencode/src/kilocode/config/overlay.ts +++ b/packages/opencode/src/kilocode/config/overlay.ts @@ -88,6 +88,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 58bb3e8de26..5ad85cf9747 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.globalConfig.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.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.globalConfig.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 00000000000..f3d90021563 --- /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 cf605c78bf7..994a1b47a11 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,9 @@ function View(props: { api: TuiPluginApi }) { name: list.at(-1) ?? "", } }) + 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, @@ -167,19 +172,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/opencode/test/fixture/tui-plugin.ts b/packages/opencode/test/fixture/tui-plugin.ts index 06706bc96be..13860c0d0b4 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 adffc2e086a..beb59238d9b 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/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9e560d5001e..c9579f87ef7 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2555,6 +2555,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 60691352a16..e47e125728e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -33415,6 +33415,9 @@ "hide_prompt_training_models": { "type": "boolean" }, + "privacy_mode": { + "type": "boolean" + }, "sandbox": { "type": "object", "properties": { diff --git a/packages/tui/src/plugin/adapters.tsx b/packages/tui/src/plugin/adapters.tsx index c8d03f33a34..9e970cad9d4 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 },