Compare commits

...

1 Commits

Author SHA1 Message Date
Alex Alecu 0baefa25fe feat: Add Kilo CLI notifications when user input is needed 2026-02-20 17:36:13 +02:00
6 changed files with 128 additions and 1 deletions
+24 -1
View File
@@ -42,6 +42,8 @@ import { writeHeapSnapshot } from "v8"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { registerKiloCommands } from "@/kilocode/kilo-commands" // kilocode_change
import { initializeTUIDependencies } from "@kilocode/kilo-gateway/tui" // kilocode_change
import { FocusProvider, useFocused } from "@tui/util/focus" // kilocode_change
import { notify } from "@tui/util/notify" // kilocode_change
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
// can't set raw mode if not a TTY
@@ -159,7 +161,9 @@ export function tui(input: {
<FrecencyProvider>
<PromptHistoryProvider>
<PromptRefProvider>
<App />
<FocusProvider>
<App />
</FocusProvider>
</PromptRefProvider>
</PromptHistoryProvider>
</FrecencyProvider>
@@ -740,6 +744,25 @@ function App() {
})
})
// kilocode_change start - Terminal notifications when input is needed and window is unfocused
const focus = useFocused()
sdk.event.on("permission.asked", (evt) => {
if (focus.focused()) return
notify(
"Permission Required",
`${evt.properties.permission} approval needed`,
sync.data.config,
evt.properties.sessionID,
)
})
sdk.event.on("question.asked", (evt) => {
if (focus.focused()) return
const body = evt.properties.questions[0]?.header ?? "The agent is asking a question"
notify("Question", body, sync.data.config, evt.properties.sessionID)
})
// kilocode_change end
return (
<box
width={dimensions().width}
@@ -0,0 +1,24 @@
// kilocode_change - new file
import { useRenderer } from "@opentui/solid"
import { createSignal, onCleanup } from "solid-js"
import { createSimpleContext } from "../context/helper"
export const { use: useFocused, provider: FocusProvider } = createSimpleContext({
name: "Focus",
init: () => {
const renderer = useRenderer()
const [focused, setFocused] = createSignal(true)
const onFocus = () => setFocused(true)
const onBlur = () => setFocused(false)
renderer.on("focus", onFocus)
renderer.on("blur", onBlur)
onCleanup(() => {
renderer.removeListener("focus", onFocus)
renderer.removeListener("blur", onBlur)
})
return { focused }
},
})
@@ -0,0 +1,41 @@
// kilocode_change - new file
import type { Config } from "@kilocode/sdk/v2"
const alertedAt = new Map<string, number>()
const COOLDOWN_MS = 5000
/** Strip control characters to prevent escape sequence injection in OSC payloads. */
const sanitize = (s: string) => s.replace(/[\x00-\x1f\x7f-\x9f]/g, "")
export function notify(title: string, body: string, config: Config, sessionID: string) {
const now = Date.now()
const last = alertedAt.get(sessionID) ?? 0
if (now - last < COOLDOWN_MS) return
// Prune stale entries to prevent unbounded growth of the alertedAt map
for (const [key, time] of alertedAt) {
if (now - time >= COOLDOWN_MS) alertedAt.delete(key)
}
alertedAt.set(sessionID, now)
// bell and osc default to enabled (opt-out via `notifications.bell: false` / `notifications.osc: false`)
const bell = config.tui?.notifications?.bell !== false
const osc = config.tui?.notifications?.osc !== false
if (bell) {
process.stdout.write("\x07")
}
if (osc) {
const safe = `${sanitize(title)}: ${sanitize(body)}`
// OSC 9 is widely supported; terminals that don't understand it simply ignore it
process.stdout.write(`\x1b]9;${safe}\x07`)
// Kitty uses its own OSC 99 notification protocol
if (process.env["TERM_PROGRAM"] === "kitty") {
process.stdout.write(`\x1b]99;i=1:d=0;${safe}\x1b\\`)
}
}
}
+12
View File
@@ -1015,6 +1015,18 @@ export namespace Config {
.enum(["auto", "stacked"])
.optional()
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
// kilocode_change start
notifications: z
.object({
bell: z.boolean().optional().describe("Enable terminal bell when input is needed and window is unfocused"),
osc: z
.boolean()
.optional()
.describe("Enable OSC 9/99 notifications for iTerm2/Kitty when input is needed and window is unfocused"),
})
.optional()
.describe("Notification settings for the TUI"),
// kilocode_change end
})
export const Server = z
+13
View File
@@ -1719,6 +1719,19 @@ export type Config = {
* Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column
*/
diff_style?: "auto" | "stacked"
/**
* Notification settings for the TUI
*/
notifications?: {
/**
* Enable terminal bell when input is needed and window is unfocused
*/
bell?: boolean
/**
* Enable OSC 9/99 notifications for iTerm2/Kitty when input is needed and window is unfocused
*/
osc?: boolean
}
}
server?: ServerConfig
/**
+14
View File
@@ -10157,6 +10157,20 @@
"description": "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column",
"type": "string",
"enum": ["auto", "stacked"]
},
"notifications": {
"description": "Notification settings for the TUI",
"type": "object",
"properties": {
"bell": {
"description": "Enable terminal bell when input is needed and window is unfocused",
"type": "boolean"
},
"osc": {
"description": "Enable OSC 9/99 notifications for iTerm2/Kitty when input is needed and window is unfocused",
"type": "boolean"
}
}
}
}
},