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"]) + }) +})