Files
deepseek-harness/packages/ui/desktop/src/index.ts
NI0317 c4584b5c19 feat(ui): rebuild desktop renderer on a shared trace graph and design tokens
Replace the full-innerHTML render loop with a static shell plus
per-region updates, so composer drafts, fold state, focus, and scroll
survive streaming turns. Fold session events into one trace graph
consumed by Chat, Trajectory, Waterfall, and the shared inspector
drawer, with live ACP updates patched into a keyed live-turn region.

Align the visual system with a tokenized design spec: a 4px spacing
base with fixed control/row height steps, foreground-derived text
tiers and borders (color-mix), neutral interaction overlays, tiered
motion durations with a reduced-motion collapse, hover-revealed
scrollbars, and drawer-aware layout elasticity. Localize trajectory
role chips and row previews.

The renderer entry (app.ts) joins the coverage exclude list as a
self-executing DOM bootstrap: jsdom lifecycle specs exercise its
behavior, and extractable logic lives in covered modules
(trace-graph.ts, renderer-content.ts).
2026-07-19 01:37:04 +08:00

368 lines
11 KiB
TypeScript

/**
* Shared contracts for the DeepSeek Harness desktop app.
*
* The package starts with view and lifecycle contracts so Electron main,
* preload, and renderer code can evolve without copying product decisions from
* design notes.
*
* @module @deepseek-ai/dsh-desktop
*/
/** Main analysis surfaces in the DeepSeek Harness session view. */
export const DESKTOP_SURFACES = ['chat', 'trajectory', 'waterfall', 'context', 'compare', 'dev'] as const
/** Main analysis surfaces in the DeepSeek Harness session view. */
export type DesktopSurface = (typeof DESKTOP_SURFACES)[number]
/** Right-side inspector tabs, ordered as rendered. */
export const INSPECTOR_TABS = ['input', 'output', 'metadata', 'feedback'] as const
/** Right-side inspector tabs. */
export type InspectorTab = (typeof INSPECTOR_TABS)[number]
/** What a middle surface is primarily for. */
export type SurfacePurpose = 'drive' | 'navigate' | 'timing' | 'request-anatomy' | 'diff' | 'development'
/** Stable class of selectable objects shared by all surfaces. */
export type InspectorTargetKind =
| 'session'
| 'run'
| 'turn'
| 'step'
| 'request'
| 'message'
| 'assistant-stream'
| 'tool-call'
| 'tool-result'
| 'context-section'
| 'waterfall-span'
| 'dev-object'
/** Stable selector fields used to derive view-independent inspector ids. */
export interface InspectorTargetKey {
readonly sessionId: string
readonly runId?: string
readonly kind: InspectorTargetKind
readonly eventSeq?: number
readonly syntheticId?: string
}
/** A selected object that can open the inspector drawer. */
export interface InspectorTarget {
/** Stable view-independent id, usually derived from session id and event seq. */
readonly id: string
/** The selected object's normalized kind. */
readonly kind: InspectorTargetKind
/** Human-readable title shown in the inspector header. */
readonly title: string
/** Optional compact subtitle, such as `turn 1 step 2` or a duration. */
readonly subtitle?: string
}
/** A visible tab in the inspector for a selected object. */
export interface InspectorTabState {
readonly tab: InspectorTab
readonly available: boolean
readonly summary?: string
readonly fullPayloadRef?: string
readonly canCopy: boolean
}
/** Right drawer state. It is absent until the user selects an object. */
export interface InspectorState {
readonly open: boolean
readonly target?: InspectorTarget
readonly activeTab: InspectorTab
readonly tabs: readonly InspectorTabState[]
}
/** Decides whether the inspector is the right place for complete detail. */
export interface SurfacePolicy {
/** True when a surface is mainly for navigation, explanation, or comparison. */
readonly summaryFirst: boolean
/** True when the surface may show a bounded preview inline. */
readonly inlinePreview: boolean
/** True when full raw payloads belong only in the inspector. */
readonly fullDetailInInspector: boolean
}
/** Static description of one middle surface. */
export interface SurfaceDefinition extends SurfacePolicy {
readonly id: DesktopSurface
readonly label: string
readonly purpose: SurfacePurpose
readonly primaryQuestion: string
readonly ownsComposer: boolean
}
/** Lifecycle state for the managed Harness runtime subprocess. */
export type RuntimeState = 'stopped' | 'starting' | 'running' | 'restart-needed' | 'stopping' | 'error'
/** Metadata attached to a run so replay and compare can explain provenance. */
export interface RunArtifact {
readonly runId: string
readonly sessionId: string
readonly createdAt: number
readonly cwd: string
readonly gitCommit?: string
readonly gitDirty?: boolean
readonly runtimeConfigHash?: string
readonly parentRunId?: string
readonly replayOf?: {
readonly runId: string
readonly turn?: number
readonly mode: 'prompt' | 'session-boundary'
}
}
/** Structural node kinds for the trajectory navigator. */
export type TrajectoryNodeKind =
| 'session'
| 'turn'
| 'step'
| 'request'
| 'assistant'
| 'tool'
| 'context'
| 'error'
/** Status shown on structural and timing views. */
export type NodeStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'unknown'
/** A bounded structural row. Full payloads stay in the inspector. */
export interface TrajectoryNode {
readonly id: string
readonly kind: TrajectoryNodeKind
readonly title: string
readonly status: NodeStatus
readonly depth: number
readonly target: InspectorTarget
readonly eventSeqs: readonly number[]
readonly preview?: string
readonly badges?: readonly string[]
readonly children?: readonly TrajectoryNode[]
}
/** Context sections reconstructed at a selected request boundary. */
export type ContextSectionKind =
| 'config'
| 'system'
| 'message-prefix'
| 'derived-history'
| 'context-message'
| 'steering-message'
| 'tool-schemas'
| 'compaction'
| 'request-delta'
/** Summary row in the Context surface. Full text/schema/JSON is inspector-owned. */
export interface ContextSection {
readonly id: string
readonly kind: ContextSectionKind
readonly title: string
readonly target: InspectorTarget
readonly eventSeqs: readonly number[]
readonly preview?: string
readonly tokenEstimate?: number
readonly changedSincePreviousRequest?: boolean
}
/** Timing bar shown in Waterfall. */
export interface WaterfallSpan {
readonly id: string
readonly title: string
readonly target: InspectorTarget
readonly startMs: number
readonly durationMs: number
readonly status: NodeStatus
readonly parentId?: string
}
/** A user-authored note attached to a stable inspector target. */
export interface FeedbackEntry {
readonly targetId: string
readonly author: string
readonly body: string
readonly createdAt: number
}
/** Run pair used by Compare. Compare is not scoped to a single session. */
export interface ComparePair {
readonly baseline: RunArtifact
readonly candidate: RunArtifact
}
/** First Dev panel shape: agent-assisted modification plus runtime restart. */
export interface DevPanelStatus {
readonly runtimeState: RuntimeState
readonly repoDirty: boolean
readonly restartNeeded: boolean
readonly watchedPaths: readonly string[]
readonly suggestedPrompt?: string
}
/** Default author filled into new feedback entries. */
export const DEFAULT_FEEDBACK_AUTHOR = 'shentuni'
/** Default surface definitions for the first Electron implementation. */
export const SURFACE_DEFINITIONS: Record<DesktopSurface, SurfaceDefinition> = {
chat: {
id: 'chat',
label: 'Chat',
purpose: 'drive',
primaryQuestion: 'What did the user and agent say, with thinking and tool use folded into readable activity rows?',
ownsComposer: true,
summaryFirst: true,
inlinePreview: true,
fullDetailInInspector: true,
},
trajectory: {
id: 'trajectory',
label: 'Trajectory',
purpose: 'navigate',
primaryQuestion: 'Where am I in the session, turn, step, request, assistant, tool, and context structure?',
ownsComposer: true,
summaryFirst: true,
inlinePreview: true,
fullDetailInInspector: true,
},
waterfall: {
id: 'waterfall',
label: 'Waterfall',
purpose: 'timing',
primaryQuestion: 'Where did time go across model requests, tool calls, and failures?',
ownsComposer: true,
summaryFirst: true,
inlinePreview: false,
fullDetailInInspector: true,
},
context: {
id: 'context',
label: 'Context',
purpose: 'request-anatomy',
primaryQuestion: 'What exactly contributed to the selected model request boundary?',
ownsComposer: false,
summaryFirst: true,
inlinePreview: true,
fullDetailInInspector: true,
},
compare: {
id: 'compare',
label: 'Compare',
purpose: 'diff',
primaryQuestion: 'How did a candidate replay/run differ from the chosen baseline run?',
ownsComposer: false,
summaryFirst: true,
inlinePreview: true,
fullDetailInInspector: true,
},
dev: {
id: 'dev',
label: 'Dev',
purpose: 'development',
primaryQuestion: 'Which prompt, tool, plugin, and config artifacts compose the repo-bound Harness agent, and what needs reload after editing?',
ownsComposer: false,
summaryFirst: true,
inlinePreview: true,
fullDetailInInspector: false,
},
}
/** Backward-compatible alias for callers that only need policies. */
export const SURFACE_POLICIES: Record<DesktopSurface, SurfacePolicy> = SURFACE_DEFINITIONS
/**
* Returns whether selecting from a middle surface should open the inspector.
* @param surface - The active middle surface.
* @param target - The object the user selected, if any.
* @returns True when this selection should open the inspector drawer.
*/
export function opensInspector(surface: DesktopSurface, target: InspectorTarget | undefined): boolean {
return target !== undefined && SURFACE_POLICIES[surface].fullDetailInInspector
}
/**
* Returns whether the surface participates in the live session composer.
* @param surface - The active middle surface.
* @returns True for the three live session surfaces.
*/
export function ownsComposer(surface: DesktopSurface): boolean {
return SURFACE_DEFINITIONS[surface].ownsComposer
}
/**
* Full detail belongs in the inspector for trace-analysis surfaces, not the Develop artifact browser.
* @param surface - The active middle surface.
* @returns True when raw payloads should live in the inspector for this surface.
*/
export function fullDetailBelongsInInspector(surface: DesktopSurface): boolean {
return SURFACE_DEFINITIONS[surface].fullDetailInInspector
}
/**
* Create a stable, view-independent inspector id.
* @param key - Stable fields that identify the selected object.
* @returns A colon-delimited target id shared across surfaces.
*/
export function createInspectorTargetId(key: InspectorTargetKey): string {
const parts = [`session:${key.sessionId}`]
if (key.runId !== undefined) parts.push(`run:${key.runId}`)
parts.push(`kind:${key.kind}`)
if (key.eventSeq !== undefined) parts.push(`seq:${String(key.eventSeq)}`)
if (key.syntheticId !== undefined) parts.push(`synthetic:${key.syntheticId}`)
return parts.join(':')
}
/**
* Pick a useful starting inspector tab for common target kinds.
* @param target - The selected object that will be inspected.
* @returns The inspector tab that best matches the target's primary payload.
*/
export function defaultInspectorTabForTarget(target: InspectorTarget): InspectorTab {
switch (target.kind) {
case 'assistant-stream':
case 'tool-result':
return 'output'
case 'session':
case 'run':
case 'turn':
case 'step':
case 'waterfall-span':
return 'metadata'
case 'dev-object':
return 'input'
default:
return 'input'
}
}
/**
* Build closed drawer state when no node is selected.
* @returns An inspector state with no target and no visible tabs.
*/
export function closedInspectorState(): InspectorState {
return {
open: false,
activeTab: 'input',
tabs: [],
}
}
/**
* Build an open drawer state for a selected target.
* @param target - The selected object that owns the inspector content.
* @returns An open inspector state with the default tab selected.
*/
export function openInspectorState(target: InspectorTarget): InspectorState {
return {
open: true,
target,
activeTab: defaultInspectorTabForTarget(target),
tabs: INSPECTOR_TABS.map(tab => ({
tab,
available: true,
canCopy: tab !== 'feedback',
})),
}
}