mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
fix(ui): clear idle badges after viewed sessions
This commit is contained in:
parent
58ccdae220
commit
37b64e3630
8 changed files with 86 additions and 64 deletions
|
|
@ -1,9 +1,8 @@
|
|||
import { Component, Show, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { Component, Show, createMemo } from "solid-js"
|
||||
import type { Instance } from "../types/instance"
|
||||
import { getInstanceSessionIndicatorStatus } from "../stores/session-status"
|
||||
import { FolderOpen, ShieldAlert, X } from "lucide-solid"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
import { useConfig } from "../stores/preferences"
|
||||
|
||||
interface InstanceTabProps {
|
||||
instance: Instance
|
||||
|
|
@ -21,17 +20,8 @@ function getPathBasename(path: string): string {
|
|||
|
||||
const InstanceTab: Component<InstanceTabProps> = (props) => {
|
||||
const { t } = useI18n()
|
||||
const { preferences } = useConfig()
|
||||
const [now, setNow] = createSignal(Date.now())
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const timer = window.setInterval(() => setNow(Date.now()), 1000)
|
||||
onCleanup(() => window.clearInterval(timer))
|
||||
}
|
||||
|
||||
const aggregatedStatus = createMemo(() =>
|
||||
getInstanceSessionIndicatorStatus(props.instance.id, now(), preferences().keepUnseenSubagentIdleStatus),
|
||||
)
|
||||
const aggregatedStatus = createMemo(() => getInstanceSessionIndicatorStatus(props.instance.id))
|
||||
const statusClassName = createMemo(() => {
|
||||
const status = aggregatedStatus()
|
||||
if (!status) return null
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import { useDrawerChrome } from "./shell/useDrawerChrome"
|
|||
import { getRetrySeconds, getSessionRetry, getSessionStatus, shouldShowSessionStatus } from "../../stores/session-status"
|
||||
import { Maximize2, ShieldAlert } from "lucide-solid"
|
||||
import type { PromptInputApi } from "../prompt-input/types"
|
||||
import { useConfig } from "../../stores/preferences"
|
||||
|
||||
import type { LayoutMode } from "./shell/types"
|
||||
import {
|
||||
|
|
@ -91,7 +90,6 @@ interface InstanceShellProps {
|
|||
|
||||
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const { t, locale } = useI18n()
|
||||
const { preferences } = useConfig()
|
||||
const isRTL = () => locale() === "he"
|
||||
|
||||
const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH)
|
||||
|
|
@ -331,12 +329,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
|
||||
const status = getSessionStatus(props.instance.id, activeSessionId)
|
||||
const retry = getSessionRetry(props.instance.id, activeSessionId)
|
||||
const showStatus = shouldShowSessionStatus(
|
||||
props.instance.id,
|
||||
activeSessionId,
|
||||
now(),
|
||||
preferences().keepUnseenSubagentIdleStatus,
|
||||
)
|
||||
const showStatus = shouldShowSessionStatus(props.instance.id, activeSessionId)
|
||||
if (!showStatus) {
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import {
|
|||
import { getGitRepoStatus, getWorktreeSlugForParentSession } from "../stores/worktrees"
|
||||
import { getLogger } from "../lib/logger"
|
||||
import { copyToClipboard } from "../lib/clipboard"
|
||||
import { useConfig } from "../stores/preferences"
|
||||
const log = getLogger("session")
|
||||
|
||||
|
||||
|
|
@ -48,7 +47,6 @@ function formatSessionStatus(status: SessionStatus): string {
|
|||
|
||||
const SessionList: Component<SessionListProps> = (props) => {
|
||||
const { t } = useI18n()
|
||||
const { preferences } = useConfig()
|
||||
const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null)
|
||||
const [isRenaming, setIsRenaming] = createSignal(false)
|
||||
|
||||
|
|
@ -429,14 +427,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||
const needsQuestion = () => Boolean((session() as any)?.pendingQuestion)
|
||||
const needsInput = () => needsPermission() || needsQuestion()
|
||||
const statusClassName = () => (needsInput() ? "session-permission" : `session-${retry() ? "retrying" : status()}`)
|
||||
const showStatus = () =>
|
||||
needsInput() ||
|
||||
shouldShowSessionStatus(
|
||||
props.instanceId,
|
||||
rowProps.sessionId,
|
||||
now(),
|
||||
preferences().keepUnseenSubagentIdleStatus,
|
||||
)
|
||||
const showStatus = () => needsInput() || shouldShowSessionStatus(props.instanceId, rowProps.sessionId)
|
||||
const statusText = () =>
|
||||
needsPermission()
|
||||
? t("sessionList.status.needsPermission")
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import PromptInput from "../prompt-input"
|
|||
import PromptAttachmentsBar from "../prompt-input/PromptAttachmentsBar"
|
||||
import { getAttachments, removeAttachment } from "../../stores/attachments"
|
||||
import { instances } from "../../stores/instances"
|
||||
import { loadMessages, sendMessage, forkSession, renameSession, isSessionMessagesLoading, markSessionIdleSeen, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
|
||||
import { loadMessages, sendMessage, forkSession, renameSession, isSessionMessagesLoading, markViewedSessionIdleSeen, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
|
||||
import { IDLE_STATUS_VISIBILITY_MS, isSessionBusy as getSessionBusyStatus } from "../../stores/session-status"
|
||||
import { deleteMessage } from "../../stores/session-actions"
|
||||
import { showAlertDialog } from "../../stores/alerts"
|
||||
|
|
@ -17,6 +17,7 @@ import { requestData } from "../../lib/opencode-api"
|
|||
import { useI18n } from "../../lib/i18n"
|
||||
import type { PromptInputApi, PromptInsertMode } from "../prompt-input/types"
|
||||
import { clearConversationPlaybackForSession } from "../../stores/conversation-speech"
|
||||
import { useConfig } from "../../stores/preferences"
|
||||
|
||||
const log = getLogger("session")
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ interface SessionViewProps {
|
|||
|
||||
export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
const { t } = useI18n()
|
||||
const { preferences } = useConfig()
|
||||
const session = () => props.activeSessions.get(props.sessionId)
|
||||
const messagesLoading = createMemo(() => isSessionMessagesLoading(props.instanceId, props.sessionId))
|
||||
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instanceId))
|
||||
|
|
@ -80,6 +82,26 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
requestAnimationFrame(() => scrollToBottomHandle?.())
|
||||
})
|
||||
}
|
||||
|
||||
function getSeenIdleSignature(currentSession: Session, keepUnseenSubagentIdleStatus: boolean): string {
|
||||
const ids: string[] = []
|
||||
|
||||
if (currentSession.status === "idle" && typeof currentSession.idleSince === "number") {
|
||||
ids.push(`${currentSession.id}:${currentSession.idleSince}`)
|
||||
}
|
||||
|
||||
if (currentSession.parentId === null && !keepUnseenSubagentIdleStatus) {
|
||||
for (const child of props.activeSessions.values()) {
|
||||
if (child.parentId !== currentSession.id) continue
|
||||
if (child.status !== "idle") continue
|
||||
if (typeof child.idleSince !== "number") continue
|
||||
ids.push(`${child.id}:${child.idleSince}`)
|
||||
}
|
||||
}
|
||||
|
||||
return ids.sort().join("|")
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.isActive,
|
||||
|
|
@ -95,16 +117,17 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
createEffect(() => {
|
||||
const currentSession = session()
|
||||
if (!props.isActive || !currentSession) return
|
||||
if (currentSession.status !== "idle") return
|
||||
if (typeof currentSession.idleSince !== "number") return
|
||||
|
||||
const idleSince = currentSession.idleSince
|
||||
const keepUnseenSubagentIdleStatus = preferences().keepUnseenSubagentIdleStatus
|
||||
const seenIdleSignature = getSeenIdleSignature(currentSession, keepUnseenSubagentIdleStatus)
|
||||
if (!seenIdleSignature) return
|
||||
|
||||
const timeout = window.setTimeout(() => {
|
||||
const latestSession = session()
|
||||
if (!props.isActive || !latestSession) return
|
||||
if (latestSession.status !== "idle") return
|
||||
if (latestSession.idleSince !== idleSince) return
|
||||
markSessionIdleSeen(props.instanceId, latestSession.id)
|
||||
const latestKeepUnseenSubagentIdleStatus = preferences().keepUnseenSubagentIdleStatus
|
||||
if (getSeenIdleSignature(latestSession, latestKeepUnseenSubagentIdleStatus) !== seenIdleSignature) return
|
||||
markViewedSessionIdleSeen(props.instanceId, latestSession.id, latestKeepUnseenSubagentIdleStatus)
|
||||
}, IDLE_STATUS_VISIBILITY_MS)
|
||||
|
||||
onCleanup(() => window.clearTimeout(timeout))
|
||||
|
|
|
|||
|
|
@ -322,6 +322,44 @@ function markSessionIdleSeen(instanceId: string, sessionId: string): void {
|
|||
})
|
||||
}
|
||||
|
||||
function markViewedSessionIdleSeen(
|
||||
instanceId: string,
|
||||
sessionId: string,
|
||||
keepUnseenSubagentIdleStatus: boolean,
|
||||
): void {
|
||||
setSessions((prev) => {
|
||||
const instanceSessions = prev.get(instanceId)
|
||||
if (!instanceSessions) return prev
|
||||
|
||||
const viewedSession = instanceSessions.get(sessionId)
|
||||
if (!viewedSession) return prev
|
||||
|
||||
const idsToClear = new Set<string>([sessionId])
|
||||
if (viewedSession.parentId === null && !keepUnseenSubagentIdleStatus) {
|
||||
for (const session of instanceSessions.values()) {
|
||||
if (session.parentId === sessionId) idsToClear.add(session.id)
|
||||
}
|
||||
}
|
||||
|
||||
let changed = false
|
||||
const updatedSessions = new Map(instanceSessions)
|
||||
for (const id of idsToClear) {
|
||||
const session = updatedSessions.get(id)
|
||||
if (!session) continue
|
||||
if (session.status !== "idle") continue
|
||||
if (typeof session.idleSince !== "number") continue
|
||||
updatedSessions.set(id, { ...session, idleSince: null })
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (!changed) return prev
|
||||
|
||||
const next = new Map(prev)
|
||||
next.set(instanceId, updatedSessions)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function setActiveSession(instanceId: string, sessionId: string): void {
|
||||
setActiveSessionId((prev) => {
|
||||
const next = new Map(prev)
|
||||
|
|
@ -745,6 +783,7 @@ export {
|
|||
setSessionPendingPermission,
|
||||
setSessionPendingQuestion,
|
||||
markSessionIdleSeen,
|
||||
markViewedSessionIdleSeen,
|
||||
setSessionStatus,
|
||||
setActiveSession,
|
||||
|
||||
|
|
|
|||
|
|
@ -30,40 +30,34 @@ describe("idle status visibility", () => {
|
|||
const idleSince = getIdleSinceForStatusTransition("working", "idle", null, 1_000)
|
||||
|
||||
assert.equal(idleSince, 1_000)
|
||||
assert.equal(shouldShowIdleStatus({ status: "idle", idleSince, parentId: null }, 1_000), true)
|
||||
assert.equal(shouldShowIdleStatus({ status: "idle", idleSince, parentId: null }, 1_000 + IDLE_STATUS_VISIBILITY_MS), true)
|
||||
assert.equal(shouldShowIdleStatus({ status: "idle", idleSince, parentId: null }), true)
|
||||
assert.equal(shouldShowIdleStatus({ status: "idle", idleSince, parentId: null }), true)
|
||||
})
|
||||
|
||||
it("auto-hides subagent idle after the transient delay by default", () => {
|
||||
it("keeps subagent idle visible until the parent or child session is seen", () => {
|
||||
const idleSince = getIdleSinceForStatusTransition("working", "idle", null, 1_000)
|
||||
|
||||
assert.equal(shouldShowIdleStatus({ status: "idle", idleSince, parentId: "parent" }, 1_000), true)
|
||||
assert.equal(
|
||||
shouldShowIdleStatus({ status: "idle", idleSince, parentId: "parent" }, 1_000 + IDLE_STATUS_VISIBILITY_MS),
|
||||
false,
|
||||
)
|
||||
assert.equal(shouldShowIdleStatus({ status: "idle", idleSince, parentId: "parent" }), true)
|
||||
assert.equal(shouldShowIdleStatus({ status: "idle", idleSince, parentId: "parent" }), true)
|
||||
})
|
||||
|
||||
it("can keep subagent idle visible until viewed", () => {
|
||||
it("does not use the keep-unseen setting to age out visible idle markers", () => {
|
||||
const idleSince = getIdleSinceForStatusTransition("working", "idle", null, 1_000)
|
||||
|
||||
assert.equal(
|
||||
shouldShowIdleStatus({ status: "idle", idleSince, parentId: "parent" }, 1_000 + IDLE_STATUS_VISIBILITY_MS, true),
|
||||
true,
|
||||
)
|
||||
assert.equal(shouldShowIdleStatus({ status: "idle", idleSince, parentId: "parent" }), true)
|
||||
})
|
||||
|
||||
it("does not show idle for sessions that started idle", () => {
|
||||
const idleSince = getIdleSinceForStatusTransition(undefined, "idle", null, 1_000)
|
||||
|
||||
assert.equal(idleSince, null)
|
||||
assert.equal(shouldShowIdleStatus({ status: "idle", idleSince, parentId: null }, 1_000), false)
|
||||
assert.equal(shouldShowIdleStatus({ status: "idle", idleSince, parentId: null }), false)
|
||||
})
|
||||
|
||||
it("clears idle visibility when work resumes", () => {
|
||||
const idleSince = getIdleSinceForStatusTransition("idle", "working", 1_000, 2_000)
|
||||
|
||||
assert.equal(idleSince, null)
|
||||
assert.equal(shouldShowIdleStatus({ status: "working", idleSince, parentId: null }, 2_000), false)
|
||||
assert.equal(shouldShowIdleStatus({ status: "working", idleSince, parentId: null }), false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -39,8 +39,6 @@ export function getSessionRetry(instanceId: string, sessionId: string): SessionR
|
|||
|
||||
export function shouldShowIdleStatus(
|
||||
session: Pick<Session, "status" | "idleSince" | "parentId"> | null | undefined,
|
||||
now = Date.now(),
|
||||
keepUnseenSubagentIdleStatus = false,
|
||||
): boolean {
|
||||
if (!session || session.status !== "idle") {
|
||||
return false
|
||||
|
|
@ -50,18 +48,12 @@ export function shouldShowIdleStatus(
|
|||
return false
|
||||
}
|
||||
|
||||
if (session.parentId && !keepUnseenSubagentIdleStatus) {
|
||||
return now - session.idleSince < IDLE_STATUS_VISIBILITY_MS
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function shouldShowSessionStatus(
|
||||
instanceId: string,
|
||||
sessionId: string,
|
||||
now = Date.now(),
|
||||
keepUnseenSubagentIdleStatus = false,
|
||||
): boolean {
|
||||
const session = getSession(instanceId, sessionId)
|
||||
if (!session) {
|
||||
|
|
@ -72,7 +64,7 @@ export function shouldShowSessionStatus(
|
|||
return true
|
||||
}
|
||||
|
||||
return session.status !== "idle" || shouldShowIdleStatus(session, now, keepUnseenSubagentIdleStatus)
|
||||
return session.status !== "idle" || shouldShowIdleStatus(session)
|
||||
}
|
||||
|
||||
export function getRetrySeconds(next: number, now = Date.now()): number {
|
||||
|
|
@ -83,8 +75,6 @@ export type InstanceSessionIndicatorStatus = "permission" | SessionStatus
|
|||
|
||||
export function getInstanceSessionIndicatorStatus(
|
||||
instanceId: string,
|
||||
now = Date.now(),
|
||||
keepUnseenSubagentIdleStatus = false,
|
||||
): InstanceSessionIndicatorStatus | null {
|
||||
const aggregated = getInstanceSessionIndicatorStatusCached(instanceId)
|
||||
if (aggregated !== "idle") {
|
||||
|
|
@ -97,7 +87,7 @@ export function getInstanceSessionIndicatorStatus(
|
|||
}
|
||||
|
||||
for (const session of instanceSessions.values()) {
|
||||
if (shouldShowIdleStatus(session, now, keepUnseenSubagentIdleStatus)) {
|
||||
if (shouldShowIdleStatus(session)) {
|
||||
return "idle"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
isSessionParentExpanded,
|
||||
loading,
|
||||
markSessionIdleSeen,
|
||||
markViewedSessionIdleSeen,
|
||||
providers,
|
||||
sessionInfoByInstance,
|
||||
sessions,
|
||||
|
|
@ -126,6 +127,7 @@ export {
|
|||
loadMessages,
|
||||
loading,
|
||||
markSessionIdleSeen,
|
||||
markViewedSessionIdleSeen,
|
||||
providers,
|
||||
sendMessage,
|
||||
sessionInfoByInstance,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue