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