Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/reset-read-notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Support resetting read notifications from the VS Code extension settings.
92 changes: 25 additions & 67 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ import { interceptMessage } from "./kilo-provider/git-changes-request"
import { matchFollowup, recordFollowup, type Followup } from "./kilo-provider/followup-session"
import { clearCommandsCache, loadCommands } from "./kilo-provider/commands"
import { fetchMessagePage, MESSAGE_PAGE_LIMIT } from "./kilo-provider/message-page"
import {
dismissNotification,
fetchAndSendNotifications as fetchNotifications,
resetReadNotifications,
type NotificationsContext,
type NotificationsMessage,
} from "./kilo-provider/notifications"
import { childID } from "./kilo-provider/task-session"
import { VisibleTaskStreams } from "./kilo-provider/visible-task-streams"
import { handleNetworkEvent, clearNetworkWaits } from "./kilo-provider/network"
Expand Down Expand Up @@ -330,7 +337,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private pending = 0
private configWarningsShown = false
/** Cached notificationsLoaded payload */
private cachedNotificationsMessage: unknown = null
private cachedNotificationsMessage: NotificationsMessage | null = null
private pendingKiloModel: { modelID?: string; agent?: string } | null = null
private pendingReviewComments: { comments: unknown[]; autoSend: boolean }[] = []
private readyResolvers: (() => void)[] = []
Expand Down Expand Up @@ -1232,6 +1239,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "resetAllSettings":
await this.handleResetAllSettings()
break
case "resetReadNotifications":
await resetReadNotifications(this.notificationsContext())
break
case "telemetry":
TelemetryProxy.capture(message.event, message.properties)
break
Expand Down Expand Up @@ -2371,79 +2381,27 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}

/**
* Fetch Kilo news/notifications and send to webview.
* Uses the cached message pattern so the webview gets data immediately on refresh.
*/
private async fetchAndSendNotifications(): Promise<void> {
if (!this.client) {
if (this.cachedNotificationsMessage) {
// Merge the latest dismissed IDs from globalState into the cached
// message so that dismissals persisted while offline are honoured.
const persisted = this.extensionContext?.globalState.get<string[]>("kilo.dismissedNotificationIds", []) ?? []
if (persisted.length > 0) {
const cached = this.cachedNotificationsMessage as {
type: string
notifications: unknown[]
dismissedIds: string[]
}
const merged = Array.from(new Set([...cached.dismissedIds, ...persisted]))
this.cachedNotificationsMessage = { ...cached, dismissedIds: merged }
}
this.postMessage(this.cachedNotificationsMessage)
}
return
private notificationsContext(): NotificationsContext {
return {
context: this.extensionContext,
client: this.client,
cached: () => this.cachedNotificationsMessage,
set: (message) => {
this.cachedNotificationsMessage = message
},
post: (message) => this.postMessage(message),
notify: (id) => this.connectionService.notifyNotificationDismissed(id),
}
}

try {
const { data: all } = await retry(() => this.client!.kilo.notifications(undefined, { throwOnError: true }))
const notifications = all.filter((n) => !n.showIn || n.showIn.includes("extension"))
const existing = this.extensionContext?.globalState.get<string[]>("kilo.dismissedNotificationIds", []) ?? []
const active = new Set(notifications.map((n) => n.id))
// Only prune stale dismissed IDs when we have a non-empty notification
// list. An empty list may mean the API returned nothing due to being
// unauthenticated (e.g. right after logout), not that all notifications
// are gone — pruning in that case would wipe the persisted dismissals.
const dismissedIds = notifications.length > 0 ? existing.filter((id) => active.has(id)) : existing
if (dismissedIds.length !== existing.length) {
await this.extensionContext?.globalState.update("kilo.dismissedNotificationIds", dismissedIds)
}
const message = { type: "notificationsLoaded", notifications, dismissedIds }
this.cachedNotificationsMessage = message
this.postMessage(message)
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to fetch notifications:", error)
}
private async fetchAndSendNotifications(): Promise<void> {
await fetchNotifications(this.notificationsContext())
}

// Cloud session methods extracted to kilo-provider/handlers/cloud-session.ts

/**
* Persist a dismissed notification ID in globalState and push updated lists to webview.
*/
private async handleDismissNotification(notificationId: string): Promise<void> {
if (!this.extensionContext) return
const existing = this.extensionContext.globalState.get<string[]>("kilo.dismissedNotificationIds", [])
if (!existing.includes(notificationId)) {
await this.extensionContext.globalState.update("kilo.dismissedNotificationIds", [...existing, notificationId])
}
// Update the cached message so the dismiss persists even if
// fetchAndSendNotifications() fails (e.g. no client / API error).
if (this.cachedNotificationsMessage) {
const cached = this.cachedNotificationsMessage as {
type: string
notifications: unknown[]
dismissedIds: string[]
}
if (!cached.dismissedIds.includes(notificationId)) {
this.cachedNotificationsMessage = {
...cached,
dismissedIds: [...cached.dismissedIds, notificationId],
}
}
}
await this.fetchAndSendNotifications()
this.connectionService.notifyNotificationDismissed(notificationId)
await dismissNotification(this.notificationsContext(), notificationId)
}

/** Read attention settings from VS Code config and push to webview. */
Expand Down
86 changes: 86 additions & 0 deletions packages/kilo-vscode/src/kilo-provider/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as vscode from "vscode"
import type { KiloClient } from "@kilocode/sdk/v2/client"
import { retry } from "../services/cli-backend/retry"

const KEY = "kilo.dismissedNotificationIds"

interface NotificationAction {
actionText: string
actionURL: string
}

interface NotificationItem {
id: string
title: string
message: string
action?: NotificationAction
showIn?: string[]
suggestModelId?: string
}

export interface NotificationsMessage {
type: "notificationsLoaded"
notifications: NotificationItem[]
dismissedIds: string[]
}

export interface NotificationsContext {
context: vscode.ExtensionContext | undefined
client: KiloClient | null
cached: () => NotificationsMessage | null
set: (message: NotificationsMessage) => void
post: (message: NotificationsMessage) => void
notify: (id: string) => void
}

export async function fetchAndSendNotifications(ctx: NotificationsContext): Promise<void> {
if (!ctx.client) {
const cached = ctx.cached()
if (cached) {
const persisted = ctx.context?.globalState.get<string[]>(KEY, []) ?? []
const dismissedIds =
persisted.length > 0 ? Array.from(new Set([...cached.dismissedIds, ...persisted])) : cached.dismissedIds

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Resetting while the extension is offline still republishes the cached dismissed IDs

resetReadNotifications() clears globalState, but this branch treats an empty persisted list as a signal to fall back to cached.dismissedIds. If the user clicks the new settings button before the client reconnects, notificationsLoaded goes back out with the old IDs, so the notifications stay hidden even though the success toast is shown. Letting an empty persisted list win here, or special-casing the reset path, would make the new action work offline as well.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const message = { ...cached, dismissedIds }
if (message !== cached) ctx.set(message)
ctx.post(message)
}
return
}

try {
const { data: all } = await retry(() => ctx.client!.kilo.notifications(undefined, { throwOnError: true }))
const notifications = all.filter((n) => !n.showIn || n.showIn.includes("extension"))
const existing = ctx.context?.globalState.get<string[]>(KEY, []) ?? []
const active = new Set(notifications.map((n) => n.id))
const dismissedIds = notifications.length > 0 ? existing.filter((id) => active.has(id)) : existing
if (dismissedIds.length !== existing.length) await ctx.context?.globalState.update(KEY, dismissedIds)
const message = { type: "notificationsLoaded" as const, notifications, dismissedIds }
ctx.set(message)
ctx.post(message)
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to fetch notifications:", error)
}
}

export async function dismissNotification(ctx: NotificationsContext, id: string): Promise<void> {
if (!ctx.context) return
const existing = ctx.context.globalState.get<string[]>(KEY, [])
if (!existing.includes(id)) await ctx.context.globalState.update(KEY, [...existing, id])

const cached = ctx.cached()
if (cached && !cached.dismissedIds.includes(id)) {
ctx.set({
...cached,
dismissedIds: [...cached.dismissedIds, id],
})
}

await fetchAndSendNotifications(ctx)
ctx.notify(id)
}

export async function resetReadNotifications(ctx: NotificationsContext): Promise<void> {
await ctx.context?.globalState.update(KEY, undefined)
await fetchAndSendNotifications(ctx)
vscode.window.showInformationMessage("Read notifications have been reset.")
}
Original file line number Diff line number Diff line change
Expand Up @@ -357,21 +357,18 @@ const AboutKiloCodeTab: Component<AboutKiloCodeTabProps> = (props) => {
>
{language.t("settings.aboutKiloCode.resetSettings.description")}
</p>
<button
type="button"
onClick={() => vscode.postMessage({ type: "resetAllSettings" })}
style={{
background: "var(--vscode-button-background)",
color: "var(--vscode-button-foreground)",
border: "none",
padding: "6px 14px",
"border-radius": "2px",
cursor: "pointer",
"font-size": "var(--kilo-font-size-12)",
}}
>
{language.t("settings.aboutKiloCode.resetSettings.button")}
</button>
<div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
<Button variant="primary" size="small" onClick={() => vscode.postMessage({ type: "resetAllSettings" })}>
{language.t("settings.aboutKiloCode.resetSettings.button")}
</Button>
<Button
variant="secondary"
size="small"
onClick={() => vscode.postMessage({ type: "resetReadNotifications" })}
>
{language.t("settings.aboutKiloCode.resetSettings.notificationsButton")}
</Button>
</div>
</div>
</div>
)
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/ar.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/br.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/bs.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/da.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/de.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"This resets only VS Code extension-specific settings to their default values. Settings shared with the CLI, such as modes and auto-approve rules, are stored in the CLI configuration and will not be reset.",
"settings.aboutKiloCode.resetSettings.button": "Reset All Settings",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Settings Transfer",
"settings.aboutKiloCode.settingsTransfer.description":
"Export or import your settings to transfer them between VS Code instances.",
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/es.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/fr.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/it.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/ja.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/ko.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/nl.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading