feat(client): surface pending session interactions

This commit is contained in:
Turtle
2026-08-04 17:14:58 +08:00
parent 11bad56fc3
commit 00d4349eb4
40 changed files with 334 additions and 147 deletions

View File

@@ -306,6 +306,7 @@ function SearchResults({
result={result}
currentId={list.current}
onOpen={open}
t={t}
/>
))}
</div>

View File

@@ -47,6 +47,8 @@ export const zh = {
'status.running': '进行中',
'status.idle': '空闲',
'status.waitingApproval': '等待审批',
'status.planReview': '计划待审',
'status.waitingAnswer': '等待回答',
'hover.created': '创建于 {time}',
'hover.copied': '已复制',
'date.ymd': '{y}年{m}月{d}日',
@@ -105,6 +107,8 @@ export const en = {
'status.running': 'Running',
'status.idle': 'Idle',
'status.waitingApproval': 'Waiting for approval',
'status.planReview': 'Plan awaiting review',
'status.waitingAnswer': 'Waiting for answer',
'hover.created': 'Created {time}',
'hover.copied': 'Copied',
'date.ymd': '{y}-{m}-{d}',

View File

@@ -166,14 +166,29 @@ export function ProjectRowItem({ group, onToggle, onCreate, actions, t }: {
)
}
/** Session status presentation; approval waiting outranks the underlying running state. */
function sessionStatus(node: SessionNode, t: RowTranslate): { state: StateDotState; label: string } {
if (node.waitingApproval) return { state: 'warning', label: t('status.waitingApproval') }
/* v8 ignore next 3 -- closed-union backstop; only reached if the status is forged */
function assertNever(value: never): never {
throw new Error(`unknown pending interaction: ${String(value)}`)
}
/** Session status presentation; pending user interaction outranks the running state. */
function sessionStatus(
node: Pick<SessionNode, 'pendingInteraction' | 'running'>,
t: RowTranslate,
): { state: StateDotState; label: string } {
switch (node.pendingInteraction) {
case 'approval': return { state: 'warning', label: t('status.waitingApproval') }
case 'plan-review': return { state: 'warning', label: t('status.planReview') }
case 'question': return { state: 'warning', label: t('status.waitingAnswer') }
case undefined: break
/* v8 ignore next -- closed PendingInteractionStatus union */
default: return assertNever(node.pendingInteraction)
}
if (node.running) return { state: 'ongoing', label: t('status.running') }
return { state: 'done', label: t('status.idle') }
}
/** Hover-card body: full title, relative time, and approval/running/idle status. */
/** Hover-card body: full title, relative time, and interaction/running/idle status. */
function SessionHoverContent({ node, now, t }: { node: SessionNode; now: number; t: RowTranslate }) {
const status = sessionStatus(node, t)
return (
@@ -215,14 +230,17 @@ export interface RowDragProps {
* @param props.result - merged local/content search row.
* @param props.currentId - selected session id.
* @param props.onOpen - open the selected session.
* @param props.t - Workspace-browser translation seat.
* @returns the result button.
*/
export function SearchResultItem({ result, currentId, onOpen }: {
export function SearchResultItem({ result, currentId, onOpen, t }: {
result: SearchResultNode
currentId: string | undefined
onOpen: (id: SearchResultNode['id']) => void
t: RowTranslate
}) {
const selected = result.id === currentId
const status = sessionStatus(result, t)
return (
<button
type="button"
@@ -232,7 +250,14 @@ export function SearchResultItem({ result, currentId, onOpen }: {
onClick={() => { onOpen(result.id) }}
>
<span className={css.searchResultHeading}>
<span className={css.slot}>{result.running && <StateDot state="ongoing" />}</span>
<span className={css.slot}>
{status.state !== 'done' && (
<>
<StateDot state={status.state} />
<span className={css.visuallyHidden}>{status.label}</span>
</>
)}
</span>
<span className={css.searchResultTitle}>{result.title}</span>
</span>
<span className={css.searchResultWorkspace}>{result.workspace}</span>
@@ -250,7 +275,7 @@ function rowHalf(e: { clientY: number; currentTarget: HTMLElement }): 'before' |
}
/**
* One top-level 34px session row: status dot (approval waiting outranks
* One top-level 34px session row: status dot (pending user interaction outranks
* running), title, relative time, and the row actions menu.
* @param props.node - derived session node.
* @param props.currentId - selected session id (row highlight).

View File

@@ -4,7 +4,8 @@
* remains visible.
*/
import type {
SessionId, SessionListState, SessionSearchResultItem, SessionSummary, WorkspaceId, WorkspaceView,
PendingInteractionStatus, SessionId, SessionListState, SessionSearchResultItem, SessionSummary,
WorkspaceId, WorkspaceView,
} from '@deepseek-ai/dsh-client-runtime/client'
/** Group key for Sessions outside every Workspace. */
@@ -20,8 +21,8 @@ export interface SessionNode {
title: string
/** The provisional blank session (renderer shows the localized New Session title). */
blank: boolean
/** The runtime Session list reports a pending approval request for this Session. */
waitingApproval: boolean
/** The runtime Session list reports an interaction awaiting this user. */
pendingInteraction?: PendingInteractionStatus
running: boolean
updatedAt: number
}
@@ -50,6 +51,8 @@ export interface SearchResultNode {
id: SessionId
title: string
workspace: string
/** The runtime Session list reports an interaction awaiting this user. */
pendingInteraction?: PendingInteractionStatus
running: boolean
snippet?: string
}
@@ -171,9 +174,9 @@ function sessionNode(s: SessionSummary): SessionNode {
id: s.id,
title: sessionTitle(s),
blank: s.blank,
waitingApproval: s.waitingApproval,
running: s.running,
updatedAt: s.updatedAt,
...(s.pendingInteraction === undefined ? {} : { pendingInteraction: s.pendingInteraction }),
}
}
@@ -324,6 +327,9 @@ export function deriveSearchResults(
title: sessionTitle(summary),
workspace: labelOf(summary),
running: summary.running,
...(summary.pendingInteraction === undefined
? {}
: { pendingInteraction: summary.pendingInteraction }),
...match === undefined ? {} : { snippet: match.snippet },
}
}),