diff --git a/.changeset/close-read-tui-news.md b/.changeset/close-read-tui-news.md
new file mode 100644
index 00000000000..ce1fd8845cd
--- /dev/null
+++ b/.changeset/close-read-tui-news.md
@@ -0,0 +1,5 @@
+---
+"@kilocode/cli": patch
+---
+
+Hide TUI news after they have been opened and add a button to close the news dialog.
diff --git a/packages/opencode/src/kilocode/components/dialog-kilo-notifications.tsx b/packages/opencode/src/kilocode/components/dialog-kilo-notifications.tsx
index 63657146ad5..9915c371fce 100644
--- a/packages/opencode/src/kilocode/components/dialog-kilo-notifications.tsx
+++ b/packages/opencode/src/kilocode/components/dialog-kilo-notifications.tsx
@@ -86,6 +86,11 @@ export function DialogKiloNotifications(props: DialogKiloNotificationsProps) {
+
+ dialog.clear()}>
+ close
+
+
)
}
diff --git a/packages/opencode/src/kilocode/components/kilo-news.tsx b/packages/opencode/src/kilocode/components/kilo-news.tsx
index 6c5508de459..6b0f5920494 100644
--- a/packages/opencode/src/kilocode/components/kilo-news.tsx
+++ b/packages/opencode/src/kilocode/components/kilo-news.tsx
@@ -9,24 +9,28 @@ import { createEffect, createMemo, createSignal, on, Show } from "solid-js"
import { useSync } from "@tui/context/sync"
import { useSDK } from "@tui/context/sdk"
import { useDialog } from "@tui/ui/dialog"
+import { useKV } from "@tui/context/kv"
import type { KilocodeNotification } from "@kilocode/kilo-gateway"
import { NotificationBanner } from "./notification-banner.js"
import { DialogKiloNotifications } from "./dialog-kilo-notifications.js"
+import { News } from "./news.js"
export function KiloNews() {
const sync = useSync()
const sdk = useSDK()
const dialog = useDialog()
+ const kv = useKV()
const [notifications, setNotifications] = createSignal([])
const [fetched, setFetched] = createSignal(false)
const isKiloConnected = createMemo(() => sync.data.provider_next.connected.includes("kilo"))
+ const unread = createMemo(() => News.unread(notifications(), kv.get(News.key, [])))
const openNewsDialog = () => {
- const items = notifications()
- if (items.length > 0) {
- dialog.replace(() => )
- }
+ const items = unread()
+ if (items.length === 0) return
+ dialog.replace(() => )
+ kv.set(News.key, News.read(items, kv.get(News.key, [])))
}
// Reactively wait for sync to complete, then fetch notifications once
@@ -53,12 +57,8 @@ export function KiloNews() {
// The banner content appears once notifications are loaded; the fixed-height
// placeholder keeps the surrounding elements stable during the async fetch.
return (
- 0} fallback={}>
-
+ 0} fallback={}>
+
)
}
diff --git a/packages/opencode/src/kilocode/components/news.ts b/packages/opencode/src/kilocode/components/news.ts
new file mode 100644
index 00000000000..26c53b85839
--- /dev/null
+++ b/packages/opencode/src/kilocode/components/news.ts
@@ -0,0 +1,19 @@
+import type { KilocodeNotification } from "@kilocode/kilo-gateway"
+
+export namespace News {
+ export const key = "news_read_ids"
+
+ function ids(value: unknown) {
+ if (!Array.isArray(value)) return []
+ return value.filter((id): id is string => typeof id === "string")
+ }
+
+ export function unread(items: KilocodeNotification[], value: unknown) {
+ const read = new Set(ids(value))
+ return items.filter((item) => !read.has(item.id))
+ }
+
+ export function read(items: KilocodeNotification[], value: unknown) {
+ return [...new Set([...ids(value), ...items.map((item) => item.id)])]
+ }
+}
diff --git a/packages/opencode/test/kilocode/news.test.ts b/packages/opencode/test/kilocode/news.test.ts
new file mode 100644
index 00000000000..4eb319349ef
--- /dev/null
+++ b/packages/opencode/test/kilocode/news.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, test } from "bun:test"
+import type { KilocodeNotification } from "@kilocode/kilo-gateway"
+import { News } from "../../src/kilocode/components/news"
+
+const item = (id: string): KilocodeNotification => ({
+ id,
+ title: id,
+ message: id,
+})
+
+describe("News", () => {
+ test("shows only notifications that have not been read", () => {
+ const items = [item("first"), item("second")]
+
+ expect(News.unread(items, ["first"])).toEqual([items[1]])
+ expect(News.unread(items, undefined)).toEqual(items)
+ expect(News.unread(items, "invalid")).toEqual(items)
+ })
+
+ test("marks every opened notification as read", () => {
+ const items = [item("first"), item("second")]
+ const read = News.read(items, ["first", "older"])
+
+ expect(read).toEqual(["first", "older", "second"])
+ expect(News.unread(items, read)).toEqual([])
+ expect(News.unread([...items, item("new")], read)).toEqual([item("new")])
+ })
+
+ test("ignores invalid persisted entries", () => {
+ expect(News.read([item("first")], [null, 1, "older", "older"])).toEqual(["older", "first"])
+ })
+})