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([