mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge pull request #38 from Kilo-Org/feat/add-user-notifications
feat: Add Kilo notifications to home screen
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
// kilocode_change - new file
|
||||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* Kilo notification schema
|
||||
*/
|
||||
export const KilocodeNotificationSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
message: z.string(),
|
||||
action: z
|
||||
.object({
|
||||
actionText: z.string(),
|
||||
actionURL: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
showIn: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export type KilocodeNotification = z.infer<typeof KilocodeNotificationSchema>
|
||||
|
||||
const NotificationsResponseSchema = z.object({
|
||||
notifications: z.array(KilocodeNotificationSchema),
|
||||
})
|
||||
|
||||
const NOTIFICATIONS_TIMEOUT_MS = 5000
|
||||
|
||||
/**
|
||||
* Fetch notifications from Kilo API
|
||||
*
|
||||
* @param options - Configuration with token and optional organization ID
|
||||
* @returns Array of notifications filtered for CLI display
|
||||
*/
|
||||
export async function fetchKilocodeNotifications(options: {
|
||||
kilocodeToken?: string
|
||||
kilocodeOrganizationId?: string
|
||||
}): Promise<KilocodeNotification[]> {
|
||||
const token = options.kilocodeToken
|
||||
if (!token) return []
|
||||
|
||||
const url = "https://api.kilo.ai/api/users/notifications"
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(NOTIFICATIONS_TIMEOUT_MS),
|
||||
})
|
||||
|
||||
if (!response.ok) return []
|
||||
|
||||
const json = await response.json()
|
||||
const result = NotificationsResponseSchema.safeParse(json)
|
||||
|
||||
if (!result.success) return []
|
||||
|
||||
// Filter to show notifications meant for CLI (or no specific target)
|
||||
// Accept "cli", "extension", or no showIn field (matches old Kilo CLI behavior)
|
||||
return result.data.notifications.filter(
|
||||
({ showIn }) => !showIn || showIn.includes("cli") || showIn.includes("extension"),
|
||||
)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export {
|
||||
promptOrganizationSelection,
|
||||
} from "./api/profile.js"
|
||||
export { fetchKiloModels } from "./api/models.js"
|
||||
export { fetchKilocodeNotifications, type KilocodeNotification } from "./api/notifications.js"
|
||||
|
||||
// ============================================================================
|
||||
// Server Routes (optional - requires hono and OpenCode dependencies)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { fetchProfile, fetchBalance } from "../api/profile.js"
|
||||
import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js"
|
||||
|
||||
// Type definitions for OpenCode dependencies (injected at runtime)
|
||||
type Hono = any
|
||||
@@ -161,4 +162,38 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/notifications",
|
||||
describeRoute({
|
||||
summary: "Get Kilo notifications",
|
||||
description: "Fetch notifications from Kilo Gateway for CLI display",
|
||||
operationId: "kilo.notifications",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Notifications list",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(KilocodeNotificationSchema)),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400, 401),
|
||||
},
|
||||
}),
|
||||
async (c: any) => {
|
||||
const auth = await Auth.get("kilo")
|
||||
if (!auth) return c.json([])
|
||||
|
||||
const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined
|
||||
if (!token) return c.json([])
|
||||
|
||||
const organizationId = auth.type === "oauth" ? auth.accountId : undefined
|
||||
const notifications = await fetchKilocodeNotifications({
|
||||
kilocodeToken: token,
|
||||
kilocodeOrganizationId: organizationId,
|
||||
})
|
||||
|
||||
return c.json(notifications)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,3 +25,9 @@ export { registerKiloCommands } from "./tui/commands/kilo-commands.js"
|
||||
export { DialogKiloTeamSelect } from "./tui/components/dialog-kilo-team-select.js"
|
||||
export { DialogKiloOrganization } from "./tui/components/dialog-kilo-organization.js"
|
||||
export { KiloAutoMethod } from "./tui/components/dialog-kilo-auto-method.js"
|
||||
export { KiloNews } from "./tui/components/kilo-news.js"
|
||||
|
||||
// ============================================================================
|
||||
// Re-exported Types
|
||||
// ============================================================================
|
||||
export type { KilocodeNotification } from "./api/notifications.js"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// kilocode_change - new file
|
||||
/**
|
||||
* Kilo Notifications Dialog
|
||||
*
|
||||
* Displays all notifications from Kilo API in a scrollable dialog.
|
||||
* Each notification shows title, message, and clickable action link.
|
||||
*/
|
||||
|
||||
import { For } from "solid-js"
|
||||
import { getTUIDependencies } from "../context.js"
|
||||
import type { KilocodeNotification } from "../../api/notifications.js"
|
||||
|
||||
interface DialogKiloNotificationsProps {
|
||||
notifications: KilocodeNotification[]
|
||||
}
|
||||
|
||||
export function DialogKiloNotifications(props: DialogKiloNotificationsProps) {
|
||||
const deps = getTUIDependencies()
|
||||
const Link = deps.Link
|
||||
const TextAttributes = deps.TextAttributes
|
||||
const dialog = deps.useDialog()
|
||||
const { theme } = deps.useTheme()
|
||||
|
||||
deps.useKeyboard((evt: any) => {
|
||||
if (evt.name === "escape" || evt.name === "return") {
|
||||
dialog.clear()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
News
|
||||
</text>
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<scrollbox maxHeight={15} flexGrow={1}>
|
||||
<box gap={2} paddingBottom={1}>
|
||||
<For each={props.notifications}>
|
||||
{(notification) => (
|
||||
<box gap={0}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.info}>*</text>
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
{notification.title}
|
||||
</text>
|
||||
</box>
|
||||
<box paddingLeft={2}>
|
||||
<text fg={theme.textMuted} wrapMode="word">
|
||||
{notification.message}
|
||||
</text>
|
||||
{notification.action && (
|
||||
<box flexDirection="row" marginTop={1}>
|
||||
<Link href={notification.action.actionURL} fg={theme.primary}>
|
||||
[{notification.action.actionText}]
|
||||
</Link>
|
||||
</box>
|
||||
)}
|
||||
</box>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</scrollbox>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// kilocode_change - new file
|
||||
/**
|
||||
* Kilo News Component
|
||||
*
|
||||
* Self-contained component that fetches and displays Kilo news/notifications.
|
||||
* Shows a banner on the home screen; clicking opens a dialog with all news items.
|
||||
*/
|
||||
|
||||
import { createMemo, createSignal, onMount, Show } from "solid-js"
|
||||
import { getTUIDependencies } from "../context.js"
|
||||
import type { KilocodeNotification } from "../../api/notifications.js"
|
||||
import { NotificationBanner } from "./notification-banner.js"
|
||||
import { DialogKiloNotifications } from "./dialog-kilo-notifications.js"
|
||||
|
||||
export function KiloNews() {
|
||||
const deps = getTUIDependencies()
|
||||
const sync = deps.useSync()
|
||||
const sdk = deps.useSDK()
|
||||
const dialog = deps.useDialog()
|
||||
|
||||
const [notifications, setNotifications] = createSignal<KilocodeNotification[]>([])
|
||||
const isKiloConnected = createMemo(() => sync.data.provider_next.connected.includes("kilo"))
|
||||
|
||||
const openNewsDialog = () => {
|
||||
const items = notifications()
|
||||
if (items.length > 0) {
|
||||
dialog.replace(() => <DialogKiloNotifications notifications={items} />)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Wait for sync to complete
|
||||
await new Promise<void>((resolve) => {
|
||||
const check = () => {
|
||||
if (sync.status === "complete") resolve()
|
||||
else setTimeout(check, 100)
|
||||
}
|
||||
check()
|
||||
})
|
||||
|
||||
if (!isKiloConnected()) return
|
||||
|
||||
const result = await sdk.client.kilo.notifications()
|
||||
const items = result.data
|
||||
if (items && items.length > 0) {
|
||||
setNotifications(items)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={notifications().length > 0}>
|
||||
<NotificationBanner
|
||||
notification={notifications()[0]}
|
||||
totalCount={notifications().length}
|
||||
onClick={openNewsDialog}
|
||||
/>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// kilocode_change - new file
|
||||
/**
|
||||
* Kilo Notification Banner
|
||||
*
|
||||
* Displays a notification teaser on the home screen.
|
||||
* Clicking opens the full notifications dialog.
|
||||
*
|
||||
* Layout:
|
||||
* * Title (N new)
|
||||
* Message text with word wrap...
|
||||
*/
|
||||
|
||||
import { Show } from "solid-js"
|
||||
import { getTUIDependencies } from "../context.js"
|
||||
import type { KilocodeNotification } from "../../api/notifications.js"
|
||||
|
||||
interface NotificationBannerProps {
|
||||
notification: KilocodeNotification
|
||||
totalCount: number
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export function NotificationBanner(props: NotificationBannerProps) {
|
||||
const deps = getTUIDependencies()
|
||||
const { theme } = deps.useTheme()
|
||||
|
||||
return (
|
||||
<box flexDirection="column" maxWidth="100%" onMouseUp={props.onClick}>
|
||||
{/* Line 1: Icon + Title + Count */}
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text flexShrink={0} style={{ fg: theme.info }}>
|
||||
*
|
||||
</text>
|
||||
<text flexShrink={0} style={{ fg: theme.text }}>
|
||||
{props.notification.title}
|
||||
</text>
|
||||
<Show when={props.totalCount > 0}>
|
||||
<text flexShrink={0} style={{ fg: theme.textMuted }}>
|
||||
({props.totalCount} new)
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
|
||||
{/* Line 2: Message (indented to align under title) */}
|
||||
<box paddingLeft={2}>
|
||||
<text style={{ fg: theme.textMuted }} wrapMode="word">
|
||||
{props.notification.message}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export interface TUIDependencies {
|
||||
useDialog: () => any
|
||||
useToast: () => any
|
||||
useTheme: () => any
|
||||
useSDK: () => any
|
||||
|
||||
// UI Components
|
||||
DialogAlert: any
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ declare module "solid-js" {
|
||||
box: any
|
||||
text: any
|
||||
span: any
|
||||
scrollbox: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -49,5 +49,6 @@ declare namespace JSX {
|
||||
box: any
|
||||
text: any
|
||||
span: any
|
||||
scrollbox: any
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,6 +584,7 @@ function App() {
|
||||
useDialog: useDialog,
|
||||
useToast: useToast,
|
||||
useTheme: useTheme,
|
||||
useSDK: useSDK,
|
||||
DialogAlert: DialogAlert,
|
||||
DialogSelect: DialogSelect,
|
||||
Link: Link,
|
||||
@@ -594,7 +595,7 @@ function App() {
|
||||
registerKiloCommands(useSDK)
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change - Delete OpenRouter Alert
|
||||
// kilocode_change - Delete OpenRouter Alert
|
||||
sdk.event.on(TuiEvent.CommandExecute.type, (evt) => {
|
||||
command.trigger(evt.properties.command)
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ import { usePromptRef } from "../context/prompt"
|
||||
import { Installation } from "@/installation"
|
||||
import { useKV } from "../context/kv"
|
||||
import { useCommandDialog } from "../component/dialog-command"
|
||||
import { KiloNews } from "@kilocode/kilo-gateway/tui" // kilocode_change
|
||||
|
||||
// TODO: what is the best way to do this?
|
||||
let once = false
|
||||
@@ -105,7 +106,9 @@ export function Home() {
|
||||
hint={Hint}
|
||||
/>
|
||||
</box>
|
||||
<box height={3} width="100%" maxWidth={75} alignItems="center" paddingTop={2}>
|
||||
{/* kilocode_change - KiloNews added */}
|
||||
<box width="100%" maxWidth={75} alignItems="center" paddingTop={2} gap={1}>
|
||||
<KiloNews />
|
||||
<Show when={showTips()}>
|
||||
<Tips />
|
||||
</Show>
|
||||
|
||||
@@ -9,6 +9,7 @@ This document explains how Kilocode configurations are automatically migrated to
|
||||
- [Rules Migration](#rules-migration)
|
||||
- [Workflows Migration](#workflows-migration)
|
||||
- [MCP Migration](#mcp-migration)
|
||||
- [Kilo Notifications](#kilo-notifications)
|
||||
|
||||
---
|
||||
|
||||
@@ -337,3 +338,52 @@ Kilocode MCP server configurations are migrated to Opencode's `mcp` config. See
|
||||
| Location | Description |
|
||||
| ----------------------------------------------------------- | ------------------------- |
|
||||
| VSCode extension storage `settings/cline_mcp_settings.json` | MCP server configurations |
|
||||
|
||||
---
|
||||
|
||||
# Kilo Notifications
|
||||
|
||||
When connected to Kilo Gateway, the CLI fetches and displays notifications from the Kilo API. This allows Kilo to communicate important announcements, feature updates, and tips to users.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **On startup**, if the user is authenticated with Kilo Gateway, the CLI fetches notifications from `https://api.kilo.ai/api/users/notifications`
|
||||
2. **Filtering**: Only notifications with `showIn` containing `"cli"` (or no `showIn` restriction) are displayed
|
||||
3. **Display**: The first notification is shown as a toast notification after a 2-second delay
|
||||
|
||||
## Notification Data Structure
|
||||
|
||||
```typescript
|
||||
interface KilocodeNotification {
|
||||
id: string // Unique identifier
|
||||
title: string // Notification title (e.g., "Agent skills now supported!")
|
||||
message: string // Description text
|
||||
action?: {
|
||||
actionText: string // Link text (e.g., "Learn More")
|
||||
actionURL: string // URL destination
|
||||
}
|
||||
showIn?: string[] // Target platforms: ["cli", "vscode"]
|
||||
}
|
||||
```
|
||||
|
||||
## Example Notification
|
||||
|
||||
```
|
||||
Title: Agent skills now supported!
|
||||
Message: Define reusable skills and workflows for your AI agent.
|
||||
Action: Learn More -> https://docs.kilo.ai/skills
|
||||
```
|
||||
|
||||
## Display Conditions
|
||||
|
||||
| Condition | Notifications Shown |
|
||||
| ------------------------- | ------------------- |
|
||||
| Connected to Kilo Gateway | Yes |
|
||||
| Not connected to Kilo | No |
|
||||
| No notifications from API | No |
|
||||
|
||||
## Related Files
|
||||
|
||||
- [`notifications.ts`](../../../../kilo-gateway/src/api/notifications.ts) - Fetch function and types
|
||||
- [`routes.ts`](../../../../kilo-gateway/src/server/routes.ts) - Server endpoint `/kilo/notifications`
|
||||
- [`app.tsx`](../../cli/cmd/tui/app.tsx) - TUI notification display logic
|
||||
|
||||
@@ -36,6 +36,8 @@ import type {
|
||||
GlobalEventResponses,
|
||||
GlobalHealthResponses,
|
||||
InstanceDisposeResponses,
|
||||
KiloNotificationsErrors,
|
||||
KiloNotificationsResponses,
|
||||
KiloOrganizationSetErrors,
|
||||
KiloOrganizationSetResponses,
|
||||
KiloProfileErrors,
|
||||
@@ -2120,6 +2122,25 @@ export class Kilo extends HeyApiClient {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Kilo notifications
|
||||
*
|
||||
* Fetch notifications from Kilo Gateway for CLI display
|
||||
*/
|
||||
public notifications<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
directory?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
|
||||
return (options?.client ?? this.client).get<KiloNotificationsResponses, KiloNotificationsErrors, ThrowOnError>({
|
||||
url: "/kilo/notifications",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
private _organization?: Organization
|
||||
get organization(): Organization {
|
||||
return (this._organization ??= new Organization({ client: this.client }))
|
||||
|
||||
@@ -4085,6 +4085,42 @@ export type KiloOrganizationSetResponses = {
|
||||
|
||||
export type KiloOrganizationSetResponse = KiloOrganizationSetResponses[keyof KiloOrganizationSetResponses]
|
||||
|
||||
export type KiloNotificationsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/kilo/notifications"
|
||||
}
|
||||
|
||||
export type KiloNotificationsErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type KiloNotificationsError = KiloNotificationsErrors[keyof KiloNotificationsErrors]
|
||||
|
||||
export type KiloNotificationsResponses = {
|
||||
/**
|
||||
* Notifications list
|
||||
*/
|
||||
200: Array<{
|
||||
id: string
|
||||
title: string
|
||||
message: string
|
||||
action?: {
|
||||
actionText: string
|
||||
actionURL: string
|
||||
}
|
||||
showIn?: Array<string>
|
||||
}>
|
||||
}
|
||||
|
||||
export type KiloNotificationsResponse = KiloNotificationsResponses[keyof KiloNotificationsResponses]
|
||||
|
||||
export type FindTextData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
Reference in New Issue
Block a user