diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index f63f6cb1a..cc2643cb9 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -182,6 +182,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 (!terminalTitleEnabled() || Flag.OPENCODE_DISABLE_TERMINAL_TITLE) return
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 1217bb54a..5a58f15d4 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 2528a4998..79de4f325 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
@@ -46,6 +46,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
session_status: {
[sessionID: string]: SessionStatus
}
+ session_unread: {
+ [sessionID: string]: boolean
+ }
session_diff: {
[sessionID: string]: Snapshot.FileDiff[]
}
@@ -81,6 +84,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
provider_default: {},
session: [],
session_status: {},
+ session_unread: {},
session_diff: {},
todo: {},
message: {},
@@ -93,6 +97,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
const sdk = useSDK()
+ let currentSessionID: string | undefined = undefined
sdk.event.listen((e) => {
const event = e.details
@@ -168,7 +173,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
}
@@ -341,6 +352,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([