Files
deepseek-harness/packages/ui/desktop/src/main.mjs
T
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

589 lines
20 KiB
JavaScript

import { spawn, execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, appendFileSync } from 'node:fs'
import { dirname, join, relative, resolve } from 'node:path'
import { Readable, Writable } from 'node:stream'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron'
import {
ClientSideConnection,
PROTOCOL_VERSION,
ndJsonStream,
} from '@agentclientprotocol/sdk'
const here = dirname(fileURLToPath(import.meta.url))
const packageRoot = resolve(here, '..')
const repoRoot = resolve(packageRoot, '../../..')
const sessionsRoot = resolve(repoRoot, '.sessions')
const feedbackRoot = resolve(sessionsRoot, '.desktop-feedback')
const acpConfigPath = resolve(repoRoot, 'examples/acp-agent/cordis.yml')
/** @type {BrowserWindow | undefined} */
let mainWindow
/** @type {'stopped' | 'starting' | 'running' | 'stopping' | 'error'} */
let runtimeState = 'stopped'
/** @type {import('node:child_process').ChildProcessWithoutNullStreams | undefined} */
let runtimeProcess
/** @type {ClientSideConnection | undefined} */
let acpClient
let initializeResult
let stderrTail = ''
/** @type {Map<string, {sessionId: string, loaded: boolean, cwd: string, title?: string}>} */
const activeSessions = new Map()
const replayingSessions = new Set()
function broadcast(channel, payload) {
for (const window of BrowserWindow.getAllWindows()) {
window.webContents.send(channel, payload)
}
}
function setRuntimeState(next, extra = {}) {
runtimeState = next
broadcast('runtime:status-update', runtimeStatus(extra))
}
function runtimeStatus(extra = {}) {
return {
state: runtimeState,
pid: runtimeProcess?.pid,
repoRoot,
sessionsRoot,
configPath: acpConfigPath,
initialized: initializeResult,
stderrTail,
...extra,
}
}
async function ensureRuntime() {
if (acpClient !== undefined && runtimeState === 'running') return acpClient
await startRuntime()
if (acpClient === undefined) throw new Error('ACP runtime did not start')
return acpClient
}
async function startRuntime() {
if (runtimeState === 'running' || runtimeState === 'starting') return runtimeStatus()
setRuntimeState('starting')
stderrTail = ''
activeSessions.clear()
runtimeProcess = spawn('node', [
'--import',
'tsx',
'packages/examples/acp-demo/src/bin.ts',
'--config',
relative(repoRoot, acpConfigPath),
], {
cwd: repoRoot,
env: { ...process.env },
stdio: ['pipe', 'pipe', 'pipe'],
})
runtimeProcess.stderr.setEncoding('utf8')
runtimeProcess.stderr.on('data', chunk => {
stderrTail = `${stderrTail}${chunk}`.slice(-10_000)
broadcast('runtime:stderr', { text: String(chunk), tail: stderrTail })
})
runtimeProcess.on('exit', (code, signal) => {
runtimeProcess = undefined
acpClient = undefined
initializeResult = undefined
activeSessions.clear()
setRuntimeState(code === 0 ? 'stopped' : 'error', { exit: { code, signal } })
})
const stream = ndJsonStream(
Writable.toWeb(runtimeProcess.stdin),
Readable.toWeb(runtimeProcess.stdout),
)
acpClient = new ClientSideConnection(() => ({
sessionUpdate(params) {
if (replayingSessions.has(String(params.sessionId))) return Promise.resolve()
const session = activeSessions.get(String(params.sessionId))
if (session !== undefined && session.title === undefined && params.update?.sessionUpdate === 'user_message_chunk') {
const title = textOfContent(params.update.content).trim()
if (title.length > 0) session.title = title.slice(0, 120)
}
broadcast('sessions:update', params)
return Promise.resolve()
},
async requestPermission(params) {
const options = params.options.map(option => `${option.name ?? option.optionId} (${option.kind})`)
const result = await dialog.showMessageBox(mainWindow, {
type: 'question',
buttons: [...options, 'Cancel'],
cancelId: options.length,
defaultId: 0,
title: 'Harness permission request',
message: params.toolCall.title ?? 'Tool permission request',
detail: JSON.stringify(params.toolCall.rawInput ?? params.toolCall, null, 2),
})
const option = params.options[result.response]
if (option === undefined) return { outcome: { outcome: 'cancelled' } }
return { outcome: { outcome: 'selected', optionId: option.optionId } }
},
async unstable_createElicitation(params) {
const result = await dialog.showMessageBox(mainWindow, {
type: 'question',
buttons: ['Accept', 'Cancel'],
cancelId: 1,
defaultId: 0,
title: 'Harness needs input',
message: params.message,
detail: JSON.stringify(params, null, 2),
})
return result.response === 0 ? { action: 'accept', content: {} } : { action: 'cancel' }
},
}), stream)
initializeResult = await acpClient.initialize({
protocolVersion: PROTOCOL_VERSION,
clientCapabilities: {
_meta: { terminal_output: true },
},
})
setRuntimeState('running')
return runtimeStatus()
}
async function stopRuntime() {
if (runtimeProcess === undefined) {
setRuntimeState('stopped')
return runtimeStatus()
}
setRuntimeState('stopping')
const proc = runtimeProcess
await new Promise(resolvePromise => {
proc.once('exit', resolvePromise)
proc.stdin.end()
setTimeout(() => {
if (!proc.killed) proc.kill('SIGTERM')
}, 1000).unref()
})
return runtimeStatus()
}
function walkJsonl(dir, out = []) {
if (!existsSync(dir)) return out
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name)
if (entry.isDirectory()) {
if (entry.name !== '.desktop-feedback') walkJsonl(full, out)
} else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
out.push(full)
}
}
return out
}
function readJsonl(file) {
const text = readFileSync(file, 'utf8')
const rows = []
for (const [index, line] of text.split(/\r?\n/).entries()) {
if (!line.trim()) continue
try {
rows.push(JSON.parse(line))
} catch (error) {
rows.push({ type: 'parse/error', seq: index, time: 0, data: { line, error: String(error) } })
}
}
const first = rows[0]
const header = first?.type === 'session'
? { ...first, path: file }
: { type: 'session', version: 0, id: file.split('/').at(-1)?.replace(/\.jsonl$/, ''), createdAt: 0, path: file }
const events = first?.type === 'session' ? rows.slice(1) : rows
return { header, events, rawText: text }
}
function textOfContent(content) {
if (!Array.isArray(content)) return ''
return content.map(block => {
if (block?.type === 'text' || block?.type === 'reasoning') return block.text ?? ''
if (block?.type === 'tool-call') return `[tool-call ${block.name}] ${block.arguments ?? ''}`
return JSON.stringify(block)
}).filter(Boolean).join('\n')
}
function latestHeader(events) {
return events.filter(event => event.type === 'request/header' && event.data?.header).at(-1)?.data?.header ?? {}
}
function summarizeSession(file) {
const { header, events } = readJsonl(file)
const requestHeader = latestHeader(events)
const firstUser = events.find(event => event.type === 'user/message')
const last = events.at(-1)
return {
id: String(header.id),
parentSession: header.parentSession === undefined ? undefined : String(header.parentSession),
cwd: header.cwd,
path: file,
relativePath: relative(repoRoot, file),
createdAt: header.createdAt || events[0]?.time || statSync(file).birthtimeMs,
lastActivity: last?.time || statSync(file).mtimeMs,
eventCount: events.length,
turnCount: events.filter(event => event.type === 'turn/start').length,
stepCount: events.filter(event => event.type === 'step/start').length,
toolCallCount: events.filter(event => event.type === 'tool/call').length,
model: requestHeader.config?.model,
title: textOfContent(firstUser?.data?.content).slice(0, 120) || String(header.id),
live: activeSessions.has(String(header.id)),
}
}
function listSessions() {
const persisted = walkJsonl(sessionsRoot).map(summarizeSession)
const persistedIds = new Set(persisted.map(session => session.id))
const liveOnly = [...activeSessions.values()]
.filter(session => !persistedIds.has(session.sessionId))
.map(session => ({
id: session.sessionId,
cwd: session.cwd,
path: undefined,
relativePath: undefined,
createdAt: Date.now(),
lastActivity: Date.now(),
eventCount: 0,
turnCount: 0,
stepCount: 0,
toolCallCount: 0,
model: undefined,
title: session.title ?? 'New live session',
live: true,
}))
return [...liveOnly, ...persisted].sort((a, b) => b.lastActivity - a.lastActivity)
}
function findSessionFile(sessionId) {
return walkJsonl(sessionsRoot).find(file => {
if (file.endsWith(`${sessionId}.jsonl`)) return true
try {
return String(readJsonl(file).header.id) === sessionId
} catch {
return false
}
})
}
function readTrace(sessionId) {
const file = findSessionFile(sessionId)
if (file === undefined) {
return { found: false, sessionId, header: { id: sessionId, cwd: repoRoot }, events: [], rawText: '' }
}
const trace = readJsonl(file)
const sessions = listSessions()
const summary = sessions.find(session => session.id === sessionId)
return {
found: true,
sessionId,
header: trace.header,
events: trace.events,
rawText: trace.rawText,
path: file,
relativePath: relative(repoRoot, file),
feedback: readFeedback(sessionId),
parent: summary?.parentSession === undefined ? undefined : sessions.find(session => session.id === summary.parentSession) ?? { id: summary.parentSession },
children: sessions.filter(session => session.parentSession === sessionId),
}
}
function feedbackFile(sessionId) {
return join(feedbackRoot, `${encodeURIComponent(sessionId)}.feedback.jsonl`)
}
function readFeedback(sessionId, targetId) {
const file = feedbackFile(sessionId)
if (!existsSync(file)) return []
return readFileSync(file, 'utf8')
.split(/\r?\n/)
.filter(Boolean)
.map((line, index) => {
try {
return JSON.parse(line)
} catch (error) {
return { type: 'feedback/parse-error', seq: index, time: 0, data: { line, error: String(error) } }
}
})
.filter(record => targetId === undefined || record.data?.targetId === targetId)
}
function appendFeedback(entry) {
const sessionId = String(entry.sessionId ?? '')
if (sessionId.length === 0) throw new Error('sessionId is required')
const rows = readFeedback(sessionId)
const record = {
type: 'feedback/add',
seq: rows.length,
time: Date.now(),
data: {
sessionId,
targetId: String(entry.targetId ?? `session:${sessionId}`),
targetTitle: String(entry.targetTitle ?? sessionId),
targetKind: String(entry.targetKind ?? 'session'),
author: String(entry.author ?? 'shentuni').trim() || 'shentuni',
text: String(entry.text ?? '').trim(),
},
}
if (record.data.text.length === 0) throw new Error('feedback text is required')
mkdirSync(feedbackRoot, { recursive: true })
appendFileSync(feedbackFile(sessionId), `${JSON.stringify(record)}\n`)
return record
}
async function ensureSessionLoaded(sessionId) {
if (activeSessions.has(sessionId)) return
const summary = listSessions().find(session => session.id === sessionId)
const client = await ensureRuntime()
replayingSessions.add(sessionId)
try {
await client.loadSession({ sessionId, cwd: summary?.cwd ?? repoRoot, mcpServers: [] })
activeSessions.set(sessionId, { sessionId, loaded: true, cwd: summary?.cwd ?? repoRoot })
} finally {
replayingSessions.delete(sessionId)
}
}
function devStatus() {
let dirty = false
let branch = 'unknown'
let commit = 'unknown'
const configText = readTextSafe(acpConfigPath)
const recentEvidence = summarizeRecentArtifactEvidence()
try {
dirty = execFileSync('git', ['status', '--porcelain'], { cwd: repoRoot, encoding: 'utf8' }).trim().length > 0
branch = execFileSync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' }).trim()
commit = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' }).trim()
} catch {
// Keep the UI usable if git is unavailable.
}
return {
runtime: runtimeStatus(),
git: { dirty, branch, commit },
watchedPaths: ['packages/**', 'examples/**', 'plugins/**', 'cordis.yml', 'package.json', 'pnpm-lock.yaml'],
restartNeeded: dirty,
recentPromptUses: recentEvidence.promptUses,
recentToolCalls: recentEvidence.toolCalls,
appComposition: {
name: 'DeepSeek Harness ACP agent',
entrypoint: 'packages/examples/acp-demo/src/bin.ts',
configPath: relative(repoRoot, acpConfigPath),
configText,
plugins: parseCordisPlugins(configText),
sourceFiles: [
{
label: 'ACP front door',
path: 'packages/examples/acp-demo/src/index.ts',
purpose: 'Loads the agent spine, JSONL persistence, user interaction service, and ACP bridge.',
text: readTextSafe(join(repoRoot, 'packages/examples/acp-demo/src/index.ts')),
},
{
label: 'Agent spine',
path: 'packages/examples/agent-spine-demo/src/index.ts',
purpose: 'Composes system prompt, tool registry, skills, agent registry, tasks, invariants, tool plugins, and agent loop.',
text: readTextSafe(join(repoRoot, 'packages/examples/agent-spine-demo/src/index.ts')),
},
{
label: 'System prompt service',
path: 'packages/core/system-prompt/src/index.ts',
purpose: 'Owns persona, tool order, and assembled model-facing prompt sections.',
text: readTextSafe(join(repoRoot, 'packages/core/system-prompt/src/index.ts')),
},
{
label: 'Tool registry',
path: 'packages/core/tools/src/index.ts',
purpose: 'Owns model-facing tool registration, schema validation, and tool presentation mode.',
text: readTextSafe(join(repoRoot, 'packages/core/tools/src/index.ts')),
},
],
},
}
}
function summarizeRecentArtifactEvidence() {
const promptUses = []
const toolCalls = new Map()
for (const file of walkJsonl(sessionsRoot)) {
let trace
try {
trace = readJsonl(file)
} catch {
continue
}
const sessionId = String(trace.header.id)
for (const event of trace.events) {
if (event.type === 'request/header' && event.data?.header) {
const header = event.data.header
promptUses.push({
sessionId,
relativePath: relative(repoRoot, file),
seq: event.seq,
time: event.time,
systemChars: String(header.system ?? '').length,
tools: Array.isArray(header.tools) ? header.tools.length : 0,
model: header.config?.model,
})
}
if (event.type === 'tool/call') {
const name = String(event.data?.name ?? event.data?.toolName ?? 'tool')
const existing = toolCalls.get(name) ?? {
name,
count: 0,
lastSessionId: sessionId,
lastRelativePath: relative(repoRoot, file),
lastSeq: event.seq,
lastTime: event.time,
}
existing.count += 1
if ((event.time ?? 0) >= (existing.lastTime ?? 0)) {
existing.lastSessionId = sessionId
existing.lastRelativePath = relative(repoRoot, file)
existing.lastSeq = event.seq
existing.lastTime = event.time
}
toolCalls.set(name, existing)
}
}
}
return {
promptUses: promptUses
.sort((a, b) => (b.time ?? 0) - (a.time ?? 0))
.slice(0, 8),
toolCalls: [...toolCalls.values()]
.sort((a, b) => (b.lastTime ?? 0) - (a.lastTime ?? 0)),
}
}
function readTextSafe(file) {
try {
return readFileSync(file, 'utf8')
} catch (error) {
return `# Unable to read ${file}\n${String(error)}`
}
}
function parseCordisPlugins(text) {
const plugins = []
let current
for (const line of text.split(/\r?\n/)) {
const id = line.match(/^\s*-\s+id:\s*(.+?)\s*$/)
if (id) {
if (current !== undefined) plugins.push(current)
current = { id: id[1], name: '', configPreview: '' }
continue
}
if (current === undefined) continue
const name = line.match(/^\s*name:\s*(.+?)\s*$/)
if (name) {
current.name = name[1].replace(/^['"]|['"]$/g, '')
continue
}
if (/^\s{2,}\S/.test(line) && current.configPreview.length < 1600) {
current.configPreview = `${current.configPreview}${line}\n`
}
}
if (current !== undefined) plugins.push(current)
return plugins
}
function registerIpc() {
ipcMain.handle('runtime:start', async () => startRuntime())
ipcMain.handle('runtime:stop', async () => stopRuntime())
ipcMain.handle('runtime:restart', async () => {
await stopRuntime()
return startRuntime()
})
ipcMain.handle('runtime:status', () => runtimeStatus())
ipcMain.handle('sessions:list', () => ({ root: sessionsRoot, sessions: listSessions() }))
ipcMain.handle('sessions:create', async () => {
const client = await ensureRuntime()
const response = await client.newSession({ cwd: repoRoot, mcpServers: [] })
activeSessions.set(response.sessionId, { sessionId: response.sessionId, loaded: true, cwd: repoRoot })
return { ...response, trace: readTrace(response.sessionId) }
})
ipcMain.handle('sessions:load', async (_event, { sessionId }) => {
await ensureSessionLoaded(String(sessionId))
return { sessionId, trace: readTrace(String(sessionId)) }
})
ipcMain.handle('sessions:prompt', async (_event, { sessionId, text }) => {
const id = String(sessionId)
await ensureSessionLoaded(id)
const client = await ensureRuntime()
const response = await client.prompt({
sessionId: id,
prompt: [{ type: 'text', text: String(text) }],
})
return { response, trace: readTrace(id) }
})
ipcMain.handle('sessions:cancel', async (_event, { sessionId }) => {
const client = await ensureRuntime()
await client.cancel({ sessionId: String(sessionId) })
return { ok: true }
})
ipcMain.handle('sessions:reveal', (_event, { sessionId }) => {
const file = findSessionFile(String(sessionId))
if (file === undefined) throw new Error(`session not found: ${String(sessionId)}`)
shell.showItemInFolder(file)
return { ok: true, path: file }
})
ipcMain.handle('trace:read', (_event, { sessionId }) => readTrace(String(sessionId)))
ipcMain.handle('feedback:list', (_event, { sessionId, targetId }) => readFeedback(String(sessionId), targetId === undefined ? undefined : String(targetId)))
ipcMain.handle('feedback:add', (_event, entry) => appendFeedback(entry))
ipcMain.handle('dev:status', () => devStatus())
}
async function createWindow() {
mainWindow = new BrowserWindow({
width: 1440,
height: 940,
minWidth: 1080,
minHeight: 720,
title: 'DeepSeek Harness Desktop',
titleBarStyle: 'hiddenInset',
backgroundColor: '#f5f5f7',
webPreferences: {
preload: join(here, 'preload.cjs'),
contextIsolation: true,
nodeIntegration: false,
},
})
if (process.env.VITE_DEV_SERVER_URL !== undefined) {
await mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
if (process.env.DSH_DESKTOP_OPEN_DEVTOOLS === '1') mainWindow.webContents.openDevTools({ mode: 'detach' })
} else {
const builtIndex = join(packageRoot, 'dist/index.html')
if (!existsSync(builtIndex)) {
await mainWindow.loadURL(`data:text/html,${encodeURIComponent('Run pnpm --dir packages/ui/desktop build:ui before pnpm --dir packages/ui/desktop start.')}`)
} else {
await mainWindow.loadURL(pathToFileURL(builtIndex).toString())
}
}
}
async function openWindowAndRuntime() {
await createWindow()
try {
await startRuntime()
} catch (error) {
setRuntimeState('error', { error: String(error) })
}
}
registerIpc()
app.whenReady().then(openWindowAndRuntime)
app.on('window-all-closed', () => {
void stopRuntime().finally(() => {
if (process.platform !== 'darwin') app.quit()
})
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) void openWindowAndRuntime()
})