Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/kimi_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ class LoopControl(BaseModel):
context_tokens + reserved_context_size >= max_context_size. Default is 50000."""


class NotificationsConfig(BaseModel):
"""Notification configuration for desktop alerts."""

enabled: bool = Field(default=True, description="Whether desktop notifications are enabled")
approvals: bool = Field(
default=True, description="Whether to show notifications for approval requests"
)


class MoonshotSearchConfig(BaseModel):
"""Moonshot Search configuration."""

Expand Down Expand Up @@ -131,6 +140,9 @@ class Config(BaseModel):
loop_control: LoopControl = Field(default_factory=LoopControl, description="Agent loop control")
services: Services = Field(default_factory=Services, description="Services configuration")
mcp: MCPConfig = Field(default_factory=MCPConfig, description="MCP configuration")
notifications: NotificationsConfig = Field(
default_factory=NotificationsConfig, description="Notification settings"
)

@model_validator(mode="after")
def validate_model(self) -> Self:
Expand Down
14 changes: 14 additions & 0 deletions src/kimi_cli/ui/shell/visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,21 @@ def request_approval(self, request: ApprovalRequest) -> None:
self._approval_request_queue.append(request)

if self._current_approval_request_panel is None:
# Play bell sound and send desktop notification
console.bell()
try:
from kimi_cli.utils.notifications import (
format_approval_notification,
notify,
)

notification_message = format_approval_notification(
request.action, request.description
)
notify("Kimi CLI - Approval Requested", notification_message)
except Exception:
# Notification failure should not block anything
pass
self.show_next_approval_request()

def show_next_approval_request(self) -> None:
Expand Down
64 changes: 64 additions & 0 deletions src/kimi_cli/utils/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Desktop notification utilities for terminal applications.

This module provides desktop notifications using OSC 9 escape sequences
(for modern terminals like Ghostty, iTerm2, Kitty, Windows Terminal).

Reference: https://iterm2.com/documentation-escape-codes.html
"""

from __future__ import annotations

import sys

from kimi_cli.utils.logging import logger


def notify(title: str, message: str) -> bool:
"""Send a desktop notification using OSC 9.

Uses OSC 9 escape sequences which are supported by modern terminals
including Ghostty, iTerm2, Kitty, Windows Terminal, and WezTerm.

Format: \x1b]9;{message}\x07

Reference: https://iterm2.com/documentation-escape-codes.html

Args:
title: The notification title.
message: The notification body text.

Returns:
True if notification was sent successfully, False otherwise.
"""
try:
# Include title in the message body for OSC 9 since it doesn't
# have a separate title field
full_message = f"{title}: {message}" if title else message
osc_sequence = f"\x1b]9;{full_message}\x07"
sys.stdout.write(osc_sequence)
sys.stdout.flush()
return True
except Exception as e:
logger.warning(f"Failed to send notification: {e}")
return False


def format_approval_notification(action: str, description: str, max_length: int = 100) -> str:
"""Format an approval request notification message.

Args:
action: The action being requested (e.g., "run command").
description: The description of the action.
max_length: Maximum length for the description.

Returns:
Formatted notification text.
"""
# Truncate description if too long
if len(description) > max_length:
description = description[: max_length - 3] + "..."

return f"{action}: {description}"


__all__ = ["notify", "format_approval_notification"]
1 change: 1 addition & 0 deletions tests/core/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def test_default_config_dump():
},
"services": {"moonshot_search": None, "moonshot_fetch": None},
"mcp": {"client": {"tool_call_timeout_ms": 60000}},
"notifications": {"enabled": True, "approvals": True},
}
)

Expand Down