feat(ui): add desktop harness workbench

This commit is contained in:
NI0317
2026-07-18 17:05:46 +08:00
parent 4139e093dd
commit 649183aa65
15 changed files with 5256 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Readable, Writable } from 'node:stream'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
ClientSideConnection,
PROTOCOL_VERSION,
ndJsonStream,
type Client,
type SessionNotification,
type Stream,
} from '@agentclientprotocol/sdk'
describe('desktop ACP subprocess bridge', () => {
let storageDir: string
let child: ChildProcessWithoutNullStreams | undefined
beforeEach(async () => {
storageDir = await mkdtemp(join(tmpdir(), 'dsh-desktop-acp-'))
})
afterEach(async () => {
if (child !== undefined) {
child.stdin.end()
child.kill('SIGTERM')
child = undefined
}
await rm(storageDir, { recursive: true, force: true })
})
it('initializes the real ACP runtime and creates a session without a model call', async () => {
child = spawn('node', [
'--import',
'tsx',
'packages/examples/acp-demo/src/bin.ts',
'--config',
'examples/acp-agent/cordis.yml',
], {
cwd: process.cwd(),
env: {
...process.env,
DSH_SNAPSHOT_SESSIONS_ROOT: storageDir,
},
stdio: ['pipe', 'pipe', 'pipe'],
})
let stderr = ''
child.stderr.setEncoding('utf8')
child.stderr.on('data', chunk => { stderr += String(chunk) })
const stream: Stream = ndJsonStream(
Writable.toWeb(child.stdin) as WritableStream<Uint8Array>,
Readable.toWeb(child.stdout) as ReadableStream<Uint8Array>,
)
const updates: SessionNotification[] = []
const client = new ClientSideConnection((): Client => ({
sessionUpdate(params) {
updates.push(params)
return Promise.resolve()
},
requestPermission() {
return Promise.resolve({ outcome: { outcome: 'cancelled' } })
},
unstable_createElicitation() {
return Promise.resolve({ action: 'cancel' })
},
}), stream)
const init = await client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
expect(init.agentInfo.name).toBe('deepseek-harness-acp')
const session = await client.newSession({ cwd: process.cwd(), mcpServers: [] })
expect(session.sessionId).toBeTruthy()
expect(updates).toHaveLength(0)
expect(stderr).not.toContain('Error:')
}, 20_000)
})

View File

@@ -0,0 +1,103 @@
import { describe, expect, it } from 'vitest'
import {
closedInspectorState,
createInspectorTargetId,
DEFAULT_FEEDBACK_AUTHOR,
DESKTOP_SURFACES,
fullDetailBelongsInInspector,
INSPECTOR_TABS,
openInspectorState,
opensInspector,
ownsComposer,
SURFACE_DEFINITIONS,
SURFACE_POLICIES,
type DesktopSurface,
type InspectorTarget,
} from '../src/index.ts'
const target: InspectorTarget = {
id: 'session:one:event:1',
kind: 'message',
title: 'user/message',
}
describe('desktop surface policies', () => {
it('routes trace-analysis detail through the inspector, but not Develop', () => {
for (const surface of DESKTOP_SURFACES.filter(surface => surface !== 'dev')) {
expect(opensInspector(surface, target)).toBe(true)
expect(fullDetailBelongsInInspector(surface)).toBe(true)
}
expect(opensInspector('dev', target)).toBe(false)
expect(fullDetailBelongsInInspector('dev')).toBe(false)
})
it('keeps empty selections from opening the inspector', () => {
expect(opensInspector('trajectory', undefined)).toBe(false)
})
it('keeps trajectory and context as summary-first surfaces', () => {
expect(SURFACE_POLICIES.trajectory).toMatchObject({
summaryFirst: true,
inlinePreview: true,
fullDetailInInspector: true,
})
expect(SURFACE_POLICIES.context).toMatchObject({
summaryFirst: true,
inlinePreview: true,
fullDetailInInspector: true,
})
})
it('keeps the composer scoped to chat only', () => {
for (const surface of DESKTOP_SURFACES) {
expect(ownsComposer(surface)).toBe(surface === 'chat')
}
})
it('keeps surface definitions aligned with the exported surface list', () => {
expect(Object.keys(SURFACE_DEFINITIONS).sort()).toEqual([...DESKTOP_SURFACES].sort())
expect(SURFACE_DEFINITIONS.trajectory.purpose).toBe('navigate')
expect(SURFACE_DEFINITIONS.context.purpose).toBe('request-anatomy')
expect(SURFACE_DEFINITIONS.compare.primaryQuestion).toContain('baseline')
expect(SURFACE_DEFINITIONS.dev.primaryQuestion).toContain('artifacts compose')
})
})
describe('desktop inspector contracts', () => {
it('uses stable target ids across surfaces', () => {
expect(createInspectorTargetId({
sessionId: 's1',
runId: 'r1',
kind: 'tool-call',
eventSeq: 42,
})).toBe('session:s1:run:r1:kind:tool-call:seq:42')
})
it('opens output by default for produced data and metadata for structural targets', () => {
expect(openInspectorState({
id: 'session:s1:kind:tool-result:seq:9',
kind: 'tool-result',
title: 'tool/result',
}).activeTab).toBe('output')
expect(openInspectorState({
id: 'session:s1:kind:step:seq:3',
kind: 'step',
title: 'step 1',
}).activeTab).toBe('metadata')
})
it('keeps inspector tabs ordered with feedback last', () => {
expect(INSPECTOR_TABS).toEqual(['input', 'output', 'metadata', 'feedback'])
expect(openInspectorState(target).tabs.map((tab) => tab.tab)).toEqual(INSPECTOR_TABS)
expect(closedInspectorState()).toEqual({
open: false,
activeTab: 'input',
tabs: [],
})
})
it('uses shentuni as the default feedback author', () => {
expect(DEFAULT_FEEDBACK_AUTHOR).toBe('shentuni')
})
})