From ecf74d6ca37e2e153f8a2e9b13ba4f7745e236b7 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Sat, 13 Dec 2025 16:57:55 -0800
Subject: [PATCH] feat: add unread indicator for completed sessions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Show a hollow circle indicator (○) in the session list for sessions that
completed while the user was viewing a different session. The indicator
is automatically cleared when the user navigates to that session.
---
packages/opencode/src/cli/cmd/tui/app.tsx | 9 +++++++
.../cmd/tui/component/dialog-session-list.tsx | 8 +++++-
.../opencode/src/cli/cmd/tui/context/sync.tsx | 25 ++++++++++++++++++-
3 files changed, 40 insertions(+), 2 deletions(-)
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 28e841122..3fbe73f52 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -172,6 +172,15 @@ function App() {
console.log(JSON.stringify(route.data))
})
+ // Track current session for unread indicator
+ createEffect(() => {
+ if (route.data.type === "session") {
+ sync.session.setCurrentSession(route.data.sessionID)
+ } else {
+ sync.session.setCurrentSession(undefined)
+ }
+ })
+
// Update terminal window title based on current route and session
createEffect(() => {
if (route.data.type === "home") {
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
index f5e0efa49..5c6f13d86 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
@@ -39,13 +39,19 @@ export function DialogSessionList() {
const isDeleting = toDelete() === x.id
const status = sync.data.session_status[x.id]
const isWorking = status?.type === "busy"
+ const isUnread = sync.data.session_unread[x.id]
+ const gutter = isWorking ? (
+
+ ) : isUnread ? (
+ ○
+ ) : undefined
return {
title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
bg: isDeleting ? theme.error : undefined,
value: x.id,
category,
footer: Locale.time(x.time.updated),
- gutter: isWorking ? : undefined,
+ gutter,
}
})
.slice(0, 150)
diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
index f74f787db..e9ae9e7b8 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
@@ -45,6 +45,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
session_status: {
[sessionID: string]: SessionStatus
}
+ session_unread: {
+ [sessionID: string]: boolean
+ }
session_diff: {
[sessionID: string]: Snapshot.FileDiff[]
}
@@ -80,6 +83,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
provider_default: {},
session: [],
session_status: {},
+ session_unread: {},
session_diff: {},
todo: {},
message: {},
@@ -92,6 +96,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
const sdk = useSDK()
+ let currentSessionID: string | undefined = undefined
sdk.event.listen((e) => {
const event = e.details
@@ -167,7 +172,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}
case "session.status": {
- setStore("session_status", event.properties.sessionID, event.properties.status)
+ const prev = store.session_status[event.properties.sessionID]
+ const next = event.properties.status
+ // Track if session transitioned from busy to idle while user is not viewing it
+ if (prev?.type === "busy" && next.type === "idle" && currentSessionID !== event.properties.sessionID) {
+ setStore("session_unread", event.properties.sessionID, true)
+ }
+ setStore("session_status", event.properties.sessionID, next)
break
}
@@ -334,6 +345,18 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
if (last.role === "user") return "working"
return last.time.completed ? "idle" : "working"
},
+ setCurrentSession(sessionID: string | undefined) {
+ currentSessionID = sessionID
+ if (sessionID) {
+ setStore("session_unread", sessionID, false)
+ }
+ },
+ getCurrentSession() {
+ return currentSessionID
+ },
+ markRead(sessionID: string) {
+ setStore("session_unread", sessionID, false)
+ },
async sync(sessionID: string) {
if (fullSyncedSessions.has(sessionID)) return
const [session, messages, todo, diff] = await Promise.all([