mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
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:
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -28115,6 +28115,9 @@
|
||||
"hide_prompt_training_models": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"privacy_mode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sandbox": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user