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/close-read-tui-news.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Hide TUI news after they have been opened and add a button to close the news dialog.
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export function DialogKiloNotifications(props: DialogKiloNotificationsProps) {
</For>
</box>
</scrollbox>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={() => dialog.clear()}>
<text fg={theme.selectedListItemText}>close</text>
</box>
</box>
</box>
)
}
20 changes: 10 additions & 10 deletions packages/opencode/src/kilocode/components/kilo-news.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<KilocodeNotification[]>([])
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(() => <DialogKiloNotifications notifications={items} />)
}
const items = unread()
if (items.length === 0) return
dialog.replace(() => <DialogKiloNotifications notifications={items} />)
kv.set(News.key, News.read(items, kv.get(News.key, [])))
}

// Reactively wait for sync to complete, then fetch notifications once
Expand All @@ -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 (
<Show when={notifications().length > 0} fallback={<box height={3} />}>
<NotificationBanner
notification={notifications()[0]}
totalCount={notifications().length}
onClick={openNewsDialog}
/>
<Show when={unread().length > 0} fallback={<box height={3} />}>
<NotificationBanner notification={unread()[0]} totalCount={unread().length} onClick={openNewsDialog} />
</Show>
)
}
19 changes: 19 additions & 0 deletions packages/opencode/src/kilocode/components/news.ts
Original file line number Diff line number Diff line change
@@ -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)])]
}
}
32 changes: 32 additions & 0 deletions packages/opencode/test/kilocode/news.test.ts
Original file line number Diff line number Diff line change
@@ -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"])
})
})
Loading