Skip to content

Commit ff244fd

Browse files
authored
Merge pull request #3369 from Kilo-Org/cli-show-notifications
CLI: Add support for notifications
2 parents b8091dd + ede62ee commit ff244fd

File tree

6 files changed

+195
-10
lines changed

6 files changed

+195
-10
lines changed

.changeset/major-sloths-mix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@kilocode/cli": patch
3+
---
4+
5+
Add support for showing Kilo Code notifications

cli/src/cli.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import { App } from "./ui/App.js"
77
import { logs } from "./services/logs.js"
88
import { extensionServiceAtom } from "./state/atoms/service.js"
99
import { initializeServiceEffectAtom } from "./state/atoms/effects.js"
10-
import { loadConfigAtom, mappedExtensionStateAtom } from "./state/atoms/config.js"
10+
import { loadConfigAtom, mappedExtensionStateAtom, providersAtom } from "./state/atoms/config.js"
1111
import { ciExitReasonAtom } from "./state/atoms/ci.js"
1212
import { requestRouterModelsAtom } from "./state/atoms/actions.js"
1313
import { loadHistoryAtom } from "./state/atoms/history.js"
1414
import { getTelemetryService, getIdentityManager } from "./services/telemetry/index.js"
15+
import { notificationsAtom, notificationsErrorAtom, notificationsLoadingAtom } from "./state/atoms/notifications.js"
16+
import { fetchKilocodeNotifications } from "./utils/notifications.js"
1517

1618
export interface CLIOptions {
1719
mode?: string
@@ -117,7 +119,12 @@ export class CLI {
117119
logs.debug("CLI configuration injected into extension", "CLI")
118120

119121
// Request router models after configuration is injected
120-
await this.requestRouterModels()
122+
void this.requestRouterModels()
123+
124+
if (!this.options.ci && !this.options.prompt) {
125+
// Fetch Kilocode notifications if provider is kilocode
126+
void this.fetchNotifications()
127+
}
121128

122129
this.isInitialized = true
123130
logs.info("Kilo Code CLI initialized successfully", "CLI")
@@ -283,6 +290,39 @@ export class CLI {
283290
}
284291
}
285292

293+
/**
294+
* Fetch notifications from Kilocode backend if provider is kilocode
295+
*/
296+
private async fetchNotifications(): Promise<void> {
297+
if (!this.store) {
298+
logs.warn("Cannot fetch notifications: store not available", "CLI")
299+
return
300+
}
301+
302+
try {
303+
const providers = this.store.get(providersAtom)
304+
305+
const provider = providers.find(({ provider }) => provider === "kilocode")
306+
307+
if (!provider) {
308+
logs.debug("No provider configured, skipping notification fetch", "CLI")
309+
return
310+
}
311+
312+
this.store.set(notificationsLoadingAtom, true)
313+
314+
const notifications = await fetchKilocodeNotifications(provider)
315+
316+
this.store.set(notificationsAtom, notifications)
317+
} catch (error) {
318+
const err = error instanceof Error ? error : new Error(String(error))
319+
this.store.set(notificationsErrorAtom, err)
320+
logs.error("Failed to fetch notifications", "CLI", { error })
321+
} finally {
322+
this.store.set(notificationsLoadingAtom, false)
323+
}
324+
}
325+
286326
/**
287327
* Get the ExtensionService instance
288328
*/

cli/src/state/atoms/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ export {
150150
syncConfigToExtensionEffectAtom,
151151
} from "./config-sync.js"
152152

153+
// ============================================================================
154+
// Notifications Atoms - Kilocode notifications management
155+
// ============================================================================
156+
export {
157+
// Core notifications atoms
158+
notificationsAtom,
159+
notificationsLoadingAtom,
160+
notificationsErrorAtom,
161+
} from "./notifications.js"
162+
153163
// ============================================================================
154164
// UI Atoms - Command-based UI state
155165
// ============================================================================
@@ -219,3 +229,4 @@ export type {
219229
export type { CliMessage } from "../../types/cli.js"
220230
export type { CommandSuggestion, ArgumentSuggestion } from "../../services/autocomplete.js"
221231
export type { FollowupSuggestion } from "./ui.js"
232+
export type { KilocodeNotification } from "./notifications.js"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { atom } from "jotai"
2+
3+
/**
4+
* Notification type matching the backend API response
5+
*/
6+
export interface KilocodeNotification {
7+
id: string
8+
title: string
9+
message: string
10+
action?: {
11+
actionText: string
12+
actionURL: string
13+
}
14+
}
15+
16+
/**
17+
* Core notifications atom - holds the list of notifications
18+
*/
19+
export const notificationsAtom = atom<KilocodeNotification[]>([])
20+
21+
/**
22+
* Loading state atom for notification fetching
23+
*/
24+
export const notificationsLoadingAtom = atom<boolean>(false)
25+
26+
/**
27+
* Error state atom for notification fetching
28+
*/
29+
export const notificationsErrorAtom = atom<Error | null>(null)

cli/src/ui/UI.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Refactored to use specialized hooks for better maintainability
44
*/
55

6-
import React, { useCallback, useEffect, useRef } from "react"
6+
import React, { useCallback, useEffect, useRef, useState } from "react"
77
import { Box, Text } from "ink"
88
import { useAtomValue, useSetAtom } from "jotai"
99
import { isStreamingAtom, errorAtom, addMessageAtom, messageResetCounterAtom } from "../state/atoms/ui.js"
@@ -27,6 +27,8 @@ import { AppOptions } from "./App.js"
2727
import { logs } from "../services/logs.js"
2828
import { createConfigErrorInstructions, createWelcomeMessage } from "./utils/welcomeMessage.js"
2929
import { generateUpdateAvailableMessage, getAutoUpdateStatus } from "../utils/auto-update.js"
30+
import { generateNotificationMessage } from "../utils/notifications.js"
31+
import { notificationsAtom } from "../state/atoms/notifications.js"
3032
import { useTerminal } from "../state/hooks/useTerminal.js"
3133

3234
// Initialize commands on module load
@@ -43,6 +45,8 @@ export const UI: React.FC<UIAppProps> = ({ options, onExit }) => {
4345
const theme = useTheme()
4446
const configValidation = useAtomValue(configValidationAtom)
4547
const resetCounter = useAtomValue(messageResetCounterAtom)
48+
const notifications = useAtomValue(notificationsAtom)
49+
const [versionStatus, setVersionStatus] = useState<Awaited<ReturnType<typeof getAutoUpdateStatus>>>()
4650

4751
// Initialize CI mode configuration
4852
const setCIMode = useSetAtom(setCIModeAtom)
@@ -164,21 +168,28 @@ export const UI: React.FC<UIAppProps> = ({ options, onExit }) => {
164168
}
165169
}, [options.ci, options.prompt, addMessage, configValidation])
166170

167-
// Auto-update check on mount
168-
const checkVersion = async () => {
169-
const status = await getAutoUpdateStatus()
170-
if (status.isOutdated) {
171-
addMessage(generateUpdateAvailableMessage(status))
171+
useEffect(() => {
172+
const checkVersion = async () => {
173+
setVersionStatus(await getAutoUpdateStatus())
172174
}
173-
}
174175

175-
useEffect(() => {
176176
if (!autoUpdatedCheckedRef.current && !options.ci) {
177177
autoUpdatedCheckedRef.current = true
178178
checkVersion()
179179
}
180180
}, [])
181181

182+
useEffect(() => {
183+
if (!versionStatus) return
184+
185+
if (versionStatus.isOutdated) {
186+
addMessage(generateUpdateAvailableMessage(versionStatus))
187+
} else if (notifications.length > 0 && notifications[0]) {
188+
// Only show notification if there's no pending update
189+
addMessage(generateNotificationMessage(notifications[0]))
190+
}
191+
}, [notifications, versionStatus])
192+
182193
// Exit if provider configuration is invalid
183194
useEffect(() => {
184195
if (!configValidation.valid) {

cli/src/utils/notifications.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { getKiloUrlFromToken } from "@roo-code/types"
2+
import { logs } from "../services/logs.js"
3+
import type { KilocodeNotification } from "../state/atoms/notifications.js"
4+
import type { ProviderConfig } from "../config/types.js"
5+
6+
/**
7+
* Response from the Kilocode notifications API
8+
*/
9+
interface NotificationsResponse {
10+
notifications: KilocodeNotification[]
11+
}
12+
13+
/**
14+
* Fetch notifications from the Kilocode backend
15+
*
16+
* @param provider - The provider configuration (must be a kilocode provider)
17+
* @returns Array of notifications, or empty array if fetch fails or provider is not kilocode
18+
*/
19+
export async function fetchKilocodeNotifications({
20+
provider,
21+
kilocodeToken,
22+
}: ProviderConfig): Promise<KilocodeNotification[]> {
23+
if (provider !== "kilocode") {
24+
logs.debug("Provider is not kilocode, skipping notification fetch", "fetchKilocodeNotifications", {
25+
provider,
26+
})
27+
return []
28+
}
29+
30+
if (!kilocodeToken) {
31+
logs.debug("No kilocode token found, skipping notification fetch", "fetchKilocodeNotifications")
32+
return []
33+
}
34+
35+
const url = getKiloUrlFromToken("https://api.kilocode.ai/api/users/notifications", kilocodeToken)
36+
37+
logs.debug("Fetching Kilocode notifications", "NotificationsUtil", { url })
38+
39+
const response = await fetch(url, {
40+
headers: {
41+
Authorization: `Bearer ${kilocodeToken}`,
42+
"Content-Type": "application/json",
43+
},
44+
})
45+
46+
if (!response.ok) {
47+
logs.error("Failed to fetch Kilocode notifications", "NotificationsUtil", {
48+
status: response.status,
49+
})
50+
return []
51+
}
52+
53+
const { notifications } = (await response.json()) as NotificationsResponse
54+
55+
return notifications
56+
}
57+
58+
/**
59+
* Check if a provider supports notifications
60+
*
61+
* @param provider - The provider configuration
62+
* @returns true if the provider supports notifications
63+
*/
64+
export function supportsNotifications(provider: ProviderConfig): boolean {
65+
return provider.provider === "kilocode" && !!provider.kilocodeToken
66+
}
67+
68+
/**
69+
* Generate a CLI message from a Kilocode notification
70+
*
71+
* @param notification - The notification to convert to a CLI message
72+
* @returns A CLI message object
73+
*/
74+
export function generateNotificationMessage(notification: KilocodeNotification) {
75+
const timestamp = Date.now()
76+
77+
let content = `## ${notification.title}\n\n${notification.message}`
78+
79+
if (notification.action) {
80+
content += `\n\n[${notification.action.actionText}](${notification.action.actionURL})`
81+
}
82+
83+
return {
84+
id: `notification-${notification.id}-${timestamp}`,
85+
ts: timestamp,
86+
type: "system" as const,
87+
content,
88+
}
89+
}

0 commit comments

Comments
 (0)