feat(client): surface subagent activity in sidebar
This commit is contained in:
@@ -45,6 +45,8 @@ export const zh = {
|
||||
'actions.session.aria': '会话“{name}”的操作',
|
||||
'actions.newSession.aria': '在“{name}”中新建会话',
|
||||
'status.running': '进行中',
|
||||
'status.subagentsRunning.one': '{n} 个子代理运行中',
|
||||
'status.subagentsRunning.other': '{n} 个子代理运行中',
|
||||
'status.idle': '空闲',
|
||||
'status.waitingApproval': '等待审批',
|
||||
'status.planReview': '计划待审',
|
||||
@@ -106,6 +108,8 @@ export const en = {
|
||||
'actions.session.aria': 'Session actions for {name}',
|
||||
'actions.newSession.aria': 'New session in {name}',
|
||||
'status.running': 'Running',
|
||||
'status.subagentsRunning.one': '{n} subagent running',
|
||||
'status.subagentsRunning.other': '{n} subagents running',
|
||||
'status.idle': 'Idle',
|
||||
'status.waitingApproval': 'Waiting for approval',
|
||||
'status.planReview': 'Plan awaiting review',
|
||||
|
||||
@@ -171,37 +171,67 @@ 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' | 'completed'>,
|
||||
interface SessionStatus {
|
||||
state: StateDotState
|
||||
label: string
|
||||
}
|
||||
|
||||
/** Session status presentation; pending user interaction remains primary. */
|
||||
function sessionStatuses(
|
||||
node: Pick<SessionNode, 'pendingInteraction' | 'running' | 'runningSubagentCount' | 'completed'>,
|
||||
t: RowTranslate,
|
||||
): { state: StateDotState; label: string } {
|
||||
): readonly [SessionStatus, ...SessionStatus[]] {
|
||||
const subagents: SessionStatus | undefined = node.runningSubagentCount === 0
|
||||
? undefined
|
||||
: {
|
||||
state: 'ongoing',
|
||||
label: t(
|
||||
node.runningSubagentCount === 1
|
||||
? 'status.subagentsRunning.one'
|
||||
: 'status.subagentsRunning.other',
|
||||
{ n: node.runningSubagentCount },
|
||||
),
|
||||
}
|
||||
let pending: SessionStatus | undefined
|
||||
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 'approval':
|
||||
pending = { state: 'warning', label: t('status.waitingApproval') }
|
||||
break
|
||||
case 'plan-review':
|
||||
pending = { state: 'warning', label: t('status.planReview') }
|
||||
break
|
||||
case 'question':
|
||||
pending = { state: 'warning', label: t('status.waitingAnswer') }
|
||||
break
|
||||
case undefined: break
|
||||
/* v8 ignore next -- closed PendingInteractionStatus union */
|
||||
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') }
|
||||
if (pending !== undefined) return subagents === undefined ? [pending] : [pending, subagents]
|
||||
if (node.running) {
|
||||
const primary: SessionStatus = { state: 'ongoing', label: t('status.running') }
|
||||
return subagents === undefined ? [primary] : [primary, subagents]
|
||||
}
|
||||
if (subagents !== undefined) return [subagents]
|
||||
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/completed/idle status. */
|
||||
/** Hover-card body: full title, relative time, and every relevant live status. */
|
||||
function SessionHoverContent({ node, now, t }: { node: SessionNode; now: number; t: RowTranslate }) {
|
||||
const status = sessionStatus(node, t)
|
||||
const statuses = sessionStatuses(node, t)
|
||||
return (
|
||||
<div className={css.hoverContent}>
|
||||
<div className={css.hoverTitle}>{displayTitle(node, t)}</div>
|
||||
{/* Same placeholder rule as the row's trailing cell: no timestamp
|
||||
before the first prompt. */}
|
||||
{!node.blank && <div className={css.hoverTime}>{hoverTimeLabel(node.updatedAt, now, t)}</div>}
|
||||
<div className={css.hoverStatus}>
|
||||
<StateDot state={status.state} />
|
||||
<span>{status.label}</span>
|
||||
</div>
|
||||
{statuses.map(status => (
|
||||
<div className={css.hoverStatus} key={status.label}>
|
||||
<StateDot state={status.state} />
|
||||
<span>{status.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -241,7 +271,8 @@ export function SearchResultItem({ result, currentId, onOpen, t }: {
|
||||
t: RowTranslate
|
||||
}) {
|
||||
const selected = result.id === currentId
|
||||
const status = sessionStatus(result, t)
|
||||
const statuses = sessionStatuses(result, t)
|
||||
const primaryStatus = statuses[0]
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -252,10 +283,12 @@ export function SearchResultItem({ result, currentId, onOpen, t }: {
|
||||
>
|
||||
<span className={css.searchResultHeading}>
|
||||
<span className={css.slot}>
|
||||
{(status.state !== 'done' || result.completed) && (
|
||||
{(primaryStatus.state !== 'done' || result.completed) && (
|
||||
<>
|
||||
<StateDot state={status.state} />
|
||||
<span className={css.visuallyHidden}>{status.label}</span>
|
||||
<StateDot state={primaryStatus.state} />
|
||||
{statuses.map(status => (
|
||||
<span className={css.visuallyHidden} key={status.label}>{status.label}</span>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
@@ -277,7 +310,7 @@ function rowHalf(e: { clientY: number; currentTarget: HTMLElement }): 'before' |
|
||||
|
||||
/**
|
||||
* One top-level 34px session row: status dot (pending user interaction outranks
|
||||
* running), title, relative time, and the row actions menu.
|
||||
* own or descendant activity), title, relative time, and the row actions menu.
|
||||
* @param props.node - derived session node.
|
||||
* @param props.currentId - selected session id (row highlight).
|
||||
* @param props.now - epoch ms for relative-time formatting.
|
||||
@@ -307,7 +340,8 @@ export function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork
|
||||
const row = node
|
||||
const title = displayTitle(node, t)
|
||||
const selected = node.id === currentId
|
||||
const status = sessionStatus(node, t)
|
||||
const statuses = sessionStatuses(node, t)
|
||||
const primaryStatus = statuses[0]
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
// Archive replaces the former Delete placeholder: it hides the row through
|
||||
// the registry-global archive set and never touches the session log, so it
|
||||
@@ -356,10 +390,12 @@ export function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork
|
||||
finished-but-unviewed session shows the green done reminder dot
|
||||
(cleared by opening the session). */}
|
||||
<span className={css.slot}>
|
||||
{(status.state !== 'done' || row.completed) && (
|
||||
{(primaryStatus.state !== 'done' || row.completed) && (
|
||||
<>
|
||||
<StateDot state={status.state} />
|
||||
<span className={css.visuallyHidden}>{status.label}</span>
|
||||
<StateDot state={primaryStatus.state} />
|
||||
{statuses.map(status => (
|
||||
<span className={css.visuallyHidden} key={status.label}>{status.label}</span>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* Unassigned Sessions trail under Ungrouped; only the selected blank Session
|
||||
* remains visible.
|
||||
*/
|
||||
import type {
|
||||
PendingInteractionStatus, SessionId, SessionListState, SessionSearchResultItem, SessionSummary,
|
||||
WorkspaceId, WorkspaceView,
|
||||
import {
|
||||
indexSubagentDescendants, type PendingInteractionStatus, type SessionId, type SessionListState,
|
||||
type SessionSearchResultItem, type SessionSummary, type SubagentDescendantSummary,
|
||||
type WorkspaceId, type WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** Group key for Sessions outside every Workspace. */
|
||||
@@ -24,6 +25,8 @@ export interface SessionNode {
|
||||
/** The runtime Session list reports an interaction awaiting this user. */
|
||||
pendingInteraction?: PendingInteractionStatus
|
||||
running: boolean
|
||||
/** Running descendants connected through uninterrupted subagent-origin lineage. */
|
||||
runningSubagentCount: number
|
||||
/** Finished running while not selected and not yet opened (the green "done" reminder dot). */
|
||||
completed: boolean
|
||||
updatedAt: number
|
||||
@@ -56,6 +59,8 @@ export interface SearchResultNode {
|
||||
/** The runtime Session list reports an interaction awaiting this user. */
|
||||
pendingInteraction?: PendingInteractionStatus
|
||||
running: boolean
|
||||
/** Running descendants connected through uninterrupted subagent-origin lineage. */
|
||||
runningSubagentCount: number
|
||||
/** Finished running while not selected and not yet opened (the green "done" reminder dot). */
|
||||
completed: boolean
|
||||
snippet?: string
|
||||
@@ -173,12 +178,16 @@ function groupByWorkspace(
|
||||
return groups
|
||||
}
|
||||
|
||||
function sessionNode(s: SessionSummary): SessionNode {
|
||||
function sessionNode(
|
||||
s: SessionSummary,
|
||||
descendants: ReadonlyMap<SessionId, SubagentDescendantSummary>,
|
||||
): SessionNode {
|
||||
return {
|
||||
id: s.id,
|
||||
title: sessionTitle(s),
|
||||
blank: s.blank,
|
||||
running: s.running,
|
||||
runningSubagentCount: descendants.get(s.id)?.runningCount ?? 0,
|
||||
completed: s.completed === true,
|
||||
updatedAt: s.updatedAt,
|
||||
...(s.pendingInteraction === undefined ? {} : { pendingInteraction: s.pendingInteraction }),
|
||||
@@ -207,6 +216,7 @@ export function deriveGroups(
|
||||
): GroupNode[] {
|
||||
const archived = new Set(archivedSessionIds)
|
||||
const expandedProjects = new Set(view.expandedProjects)
|
||||
const descendants = indexSubagentDescendants(list.byId)
|
||||
const currentGroup = list.current === undefined
|
||||
? undefined
|
||||
: (workspaces.find(w => w.sessionIds.includes(list.current as SessionId))?.workspaceId as string | undefined)
|
||||
@@ -223,7 +233,7 @@ export function deriveGroups(
|
||||
sessionCount: g.sessions.length,
|
||||
expanded,
|
||||
containsCurrent: g.key === currentGroup,
|
||||
sessions: expanded ? g.sessions.map(sessionNode) : [],
|
||||
sessions: expanded ? g.sessions.map(session => sessionNode(session, descendants)) : [],
|
||||
})
|
||||
}
|
||||
return groups
|
||||
@@ -240,6 +250,7 @@ export function deriveGroups(
|
||||
*/
|
||||
export function deriveFlat(list: SessionListState, archivedSessionIds: readonly SessionId[]): SessionNode[] {
|
||||
const archived = new Set(archivedSessionIds)
|
||||
const descendants = indexSubagentDescendants(list.byId)
|
||||
const rows: SessionSummary[] = []
|
||||
for (const id of list.ids) {
|
||||
const s = list.byId[id]
|
||||
@@ -247,7 +258,7 @@ export function deriveFlat(list: SessionListState, archivedSessionIds: readonly
|
||||
rows.push(s)
|
||||
}
|
||||
rows.sort(byRecency)
|
||||
return rows.map(sessionNode)
|
||||
return rows.map(session => sessionNode(session, descendants))
|
||||
}
|
||||
|
||||
/** Relative-time bucket of a session row's trailing label. */
|
||||
@@ -282,6 +293,7 @@ export function deriveSearchResults(
|
||||
const q = query.trim().toLowerCase()
|
||||
if (q === '') return { items: [], hasMore: false }
|
||||
const archived = new Set(archivedSessionIds)
|
||||
const descendants = indexSubagentDescendants(list.byId)
|
||||
|
||||
const workspaceBySession = new Map<SessionId, string>()
|
||||
for (const workspace of workspaces) {
|
||||
@@ -332,6 +344,7 @@ export function deriveSearchResults(
|
||||
title: sessionTitle(summary),
|
||||
workspace: labelOf(summary),
|
||||
running: summary.running,
|
||||
runningSubagentCount: descendants.get(summary.id)?.runningCount ?? 0,
|
||||
...(summary.pendingInteraction === undefined
|
||||
? {}
|
||||
: { pendingInteraction: summary.pendingInteraction }),
|
||||
|
||||
Reference in New Issue
Block a user