feat(web): done dot on sessions that finished while unviewed

A session that stops running while it is not the selected session arms a
green 'done' reminder dot on its sidebar row, so the operator notices a
finished background session and returns to it; opening the session clears
the dot, and a re-run re-arms it on completion.

SessionManager owns the reminder set (a sibling of the waiting-approval
bit): a running->idle edge of a non-selected session arms it, select()
consumes it, removal prunes it, and it survives connection generations.
The bit rides SessionListEntry/SessionSummary into the workspace browser
rows, which render the existing StateDot done state (running keeps the
spinner) and label the hover card '已完成/Completed'.
This commit is contained in:
GeeeekExplorer
2026-08-05 16:42:51 +08:00
parent 17ff1e0d4a
commit ffdcafb45f
10 changed files with 297 additions and 15 deletions
@@ -49,6 +49,7 @@ export const zh = {
'status.waitingApproval': '等待审批',
'status.planReview': '计划待审',
'status.waitingAnswer': '等待回答',
'status.completed': '已完成',
'hover.created': '创建于 {time}',
'hover.copied': '已复制',
'date.ymd': '{y}年{m}月{d}日',
@@ -109,6 +110,7 @@ export const en = {
'status.waitingApproval': 'Waiting for approval',
'status.planReview': 'Plan awaiting review',
'status.waitingAnswer': 'Waiting for answer',
'status.completed': 'Completed',
'hover.created': 'Created {time}',
'hover.copied': 'Copied',
'date.ymd': '{y}-{m}-{d}',
@@ -173,7 +173,7 @@ function assertNever(value: never): never {
/** Session status presentation; pending user interaction outranks the running state. */
function sessionStatus(
node: Pick<SessionNode, 'pendingInteraction' | 'running'>,
node: Pick<SessionNode, 'pendingInteraction' | 'running' | 'completed'>,
t: RowTranslate,
): { state: StateDotState; label: string } {
switch (node.pendingInteraction) {
@@ -185,10 +185,11 @@ function sessionStatus(
default: return assertNever(node.pendingInteraction)
}
if (node.running) return { state: 'ongoing', label: t('status.running') }
if (node.completed) return { state: 'done', label: t('status.completed') }
return { state: 'done', label: t('status.idle') }
}
/** Hover-card body: full title, relative time, and interaction/running/idle status. */
/** Hover-card body: full title, relative time, and interaction/running/completed/idle status. */
function SessionHoverContent({ node, now, t }: { node: SessionNode; now: number; t: RowTranslate }) {
const status = sessionStatus(node, t)
return (
@@ -251,7 +252,7 @@ export function SearchResultItem({ result, currentId, onOpen, t }: {
>
<span className={css.searchResultHeading}>
<span className={css.slot}>
{status.state !== 'done' && (
{(status.state !== 'done' || result.completed) && (
<>
<StateDot state={status.state} />
<span className={css.visuallyHidden}>{status.label}</span>
@@ -351,8 +352,11 @@ export function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork
drag.drop(rowHalf(e))
}}
>
{/* Pending interactions and running outrank the idle state; a
finished-but-unviewed session shows the green done reminder dot
(cleared by opening the session). */}
<span className={css.slot}>
{status.state !== 'done' && (
{(status.state !== 'done' || row.completed) && (
<>
<StateDot state={status.state} />
<span className={css.visuallyHidden}>{status.label}</span>
@@ -24,6 +24,8 @@ export interface SessionNode {
/** The runtime Session list reports an interaction awaiting this user. */
pendingInteraction?: PendingInteractionStatus
running: boolean
/** Finished running while not selected and not yet opened (the green "done" reminder dot). */
completed: boolean
updatedAt: number
}
@@ -54,6 +56,8 @@ export interface SearchResultNode {
/** The runtime Session list reports an interaction awaiting this user. */
pendingInteraction?: PendingInteractionStatus
running: boolean
/** Finished running while not selected and not yet opened (the green "done" reminder dot). */
completed: boolean
snippet?: string
}
@@ -175,6 +179,7 @@ function sessionNode(s: SessionSummary): SessionNode {
title: sessionTitle(s),
blank: s.blank,
running: s.running,
completed: s.completed === true,
updatedAt: s.updatedAt,
...(s.pendingInteraction === undefined ? {} : { pendingInteraction: s.pendingInteraction }),
}
@@ -330,6 +335,7 @@ export function deriveSearchResults(
...(summary.pendingInteraction === undefined
? {}
: { pendingInteraction: summary.pendingInteraction }),
completed: summary.completed === true,
...match === undefined ? {} : { snippet: match.snippet },
}
}),