Files
deepseek-harness/packages/experimental/client-ui-forge/src/client/SessionRowForge.tsx
T
Yichen Jiang 4916ce9bff feat(forge): show worktree and change request state in the Session list
The branch label left the Session header, and finding which Sessions
work in their own worktrees, or which of their change requests need
attention, meant opening each Session's tab.

- A Session row shows a branch glyph while the Session works in the
  worktree the panel created for it, and the state glyph of its most
  urgent change request with a dot for its checks. The row's hover card
  names the worktree branch and its base, then up to three change
  requests with their state and checks.
- The forgeWorktree projection follows the Session's working directory;
  its client view is the created worktree while the Session works in
  it. The forgeChangeRequests projection gains a client view with the
  bindings, so the Session summary carries both without loading a
  Session.
- The forgePanel.changeRequestStates stream serves the states of the
  change requests every listed Session binds from one Host run. It
  polls each until it is merged or closed and stores every change in
  the forge_panel_change_requests storage domain, so a restart shows
  the last known states before the next poll.
- A new Agent Note records the decision; the READMEs, the forge
  subsystem page, and the catalogs follow.
2026-10-22 15:10:29 +08:00

86 lines
3.4 KiB
TypeScript

/**
* Session row icons after the time label: a branch glyph while the Session
* works in the worktree the panel created for it, and the state glyph of its
* most urgent change request. Glyphs of open change requests stay in the
* row's subdued color with a dot for their checks; merged and closed ones keep
* their state color.
*/
import type { ReactNode } from 'react'
import { IconBranchOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
// Type-only: pulls the ui-workspace SlotMap merge (the Session row seats).
import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
import type { ChecksRollup } from '@deepseek-ai/dsh-experimental-forge/types'
import { ChangeRequestGlyph } from './glyphs.tsx'
import type { ForgePanelKey, NS } from './locales.ts'
import { useSessionForgeFacts, type SessionListChangeRequest, type SessionListInjected } from './session-list.ts'
import { TONE_LABEL } from './status.ts'
import css from './SessionList.module.css'
type Translate = PropsLocale<typeof NS>['t']
/** Copy of each checks outcome a row and its hover card name. */
export const CHECKS_LABEL: Record<Exclude<ChecksRollup, 'none'>, ForgePanelKey> = {
pending: 'change.ci.running',
success: 'change.ci.passed',
failure: 'change.ci.failedUnlisted',
}
/**
* The checks outcome a row shows for a change request: none for a merged,
* closed, or not yet polled one.
* @param entry - the change request.
* @returns the outcome.
*/
export function rowChecks(entry: SessionListChangeRequest): ChecksRollup {
if (entry.state === undefined || entry.tone === 'merged' || entry.tone === 'closed') return 'none'
return entry.state.checksRollup
}
/**
* Name a change request's state and checks, as one line.
* @param entry - the change request.
* @param t - forge panel copy.
* @returns the line.
*/
export function stateLine(entry: SessionListChangeRequest, t: Translate): string {
const state = t(TONE_LABEL[entry.tone])
const checks = rowChecks(entry)
return checks === 'none' ? state : t('list.stateChecks', { state, checks: t(CHECKS_LABEL[checks]) })
}
/** Full props of the row icons. */
export type SessionRowForgeProps =
PropsRuntime<'sidebar.session.row.trailing'> & PropsLocale<typeof NS> & InjectFace<SessionListInjected>
/**
* Render a Session row's forge icons.
* @param props - the row's Session, the shared hooks, and copy.
* @returns the icons, or nothing for a Session without a worktree or change requests.
*/
export function SessionRowForge({ sessionId, useSessions, useStates, t }: SessionRowForgeProps): ReactNode {
const facts = useSessionForgeFacts(useSessions, useStates, sessionId)
const top = facts.changeRequests[0]
return (
<>
{facts.worktree !== null && (
<span className={css.icon} role="img" aria-label={t('list.worktree')} data-forge-row-worktree="">
<IconBranchOutlineRegular size={13} />
</span>
)}
{top !== undefined && (
<span
className={css.icon}
role="img"
aria-label={t('change.stateAria', { state: stateLine(top, t) })}
data-forge-row-change={top.tone}
>
<ChangeRequestGlyph tone={top.tone} tinted={top.tone === 'merged' || top.tone === 'closed'} size={14} />
{rowChecks(top) !== 'none' && <span className={css.dot} data-checks={rowChecks(top)} />}
</span>
)}
</>
)
}