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 9bc1d79e309c..20d1d9b3ed0d 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 @@ -2,7 +2,7 @@ import { useDialog } from "@tui/ui/dialog" import { DialogSelect } from "@tui/ui/dialog-select" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" -import { createMemo, createResource, createSignal, onMount, type JSX } from "solid-js" +import { createEffect, createMemo, createResource, createSignal, onMount, type JSX } from "solid-js" import { Locale } from "@/util/locale" import { useProject } from "@tui/context/project" import { useTheme } from "../context/theme" @@ -19,6 +19,10 @@ import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed" import { WorkspaceLabel } from "./workspace-label" import { useCommandShortcut } from "../keymap" +// Track sessions the user has clicked so their dot disappears until they run again +// This persists across dialog open/close cycles +const [seenIdle, setSeenIdle] = createSignal(new Set(), { equals: false }) + export function DialogSessionList() { const dialog = useDialog() const route = useRoute() @@ -44,6 +48,25 @@ export function DialogSessionList() { const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) const sessions = createMemo(() => searchResults() ?? sync.data.session) + // Collect currently-busy session IDs reactively via memo so the effect below + // properly tracks new key additions to session_status + const busySessions = createMemo(() => + Object.entries(sync.data.session_status) + .filter(([, s]) => s.type === "busy" || s.type === "retry") + .map(([id]) => id), + ) + + // When a session starts running again, un-dismiss it so the dot reappears on completion + createEffect(() => { + for (const id of busySessions()) setSeenIdle((prev) => { prev.delete(id); return prev }) + }) + + // Auto-dismiss when the user navigates to a session (through click, dialog, etc.) + createEffect(() => { + const id = currentSessionID() + if (id) setSeenIdle((prev) => { prev.add(id); return prev }) + }) + function recover(session: NonNullable[number]>) { const workspace = project.workspace.get(session.workspaceID!) const list = () => dialog.replace(() => ) @@ -173,12 +196,22 @@ export function DialogSessionList() { const isDeleting = toDelete() === x.id const status = sync.data.session_status?.[x.id] const isWorking = status?.type === "busy" || status?.type === "retry" + const isCurrent = x.id === currentSessionID() + const needsAttention = () => { + if (status?.type === "idle") return true + const perms = sync.data.permission[x.id] ?? [] + const questions = sync.data.question[x.id] ?? [] + return perms.length > 0 || questions.length > 0 + } + const showDot = () => !isCurrent && needsAttention() && !seenIdle().has(x.id) const slot = slotByID.get(x.id) const gutter = isWorking ? () => - : slot !== undefined - ? () => {slot} - : undefined + : showDot() + ? () => + : slot !== undefined + ? () => {slot} + : undefined return { title: isDeleting ? `Press ${deleteHint()} again to confirm` : x.title, bg: isDeleting ? theme.error : undefined, @@ -226,14 +259,14 @@ export function DialogSessionList() { actions={[ ...(Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING ? [ - { - command: "session.pin.toggle", - title: "pin/unpin", - onTrigger: (option: { value: string }) => { - local.session.togglePin(option.value) - }, + { + command: "session.pin.toggle", + title: "pin/unpin", + onTrigger: (option: { value: string }) => { + local.session.togglePin(option.value) }, - ] + } + ] : []), { command: "session.delete",