feat: click file name to open file in toolcall, remove hover bg of toolcall, do not trigger sidebar any more (follow designer's instruction)

This commit is contained in:
07akioni
2026-07-28 14:24:41 +08:00
parent 4c6fb8b957
commit b926044c13
50 changed files with 649 additions and 172 deletions

View File

@@ -57,7 +57,10 @@ function stubAgent(session: Session): Agent {
/** Compose the API over real Session, Agent, Storage, Domain, and Workspace services. */
async function harness(
workspaceRoot = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-apiproxy-workspace-'))),
pickDirectory?: (signal: AbortSignal) => Promise<string | null>,
extras: {
pickDirectory?: (signal: AbortSignal) => Promise<string | null>
openPath?: (path: string, signal: AbortSignal) => Promise<void>
} = {},
) {
const ctx = new Context()
await ctx.plugin(SessionStore)
@@ -97,26 +100,29 @@ async function harness(
model: 'test-model',
cwd: workspaceRoot,
workspaceRoot,
...pickDirectory === undefined ? {} : { pickDirectory },
...extras.pickDirectory === undefined ? {} : { pickDirectory: extras.pickDirectory },
...extras.openPath === undefined ? {} : { openPath: extras.openPath },
})
return { api, ctx, storageDomain, workspaceRoot }
}
describe('host.pickDirectory', () => {
it('returns a selected path or explicit cancellation from the injected native boundary', async () => {
const selected = await harness(undefined, async () => '/tmp/project')
const selected = await harness(undefined, { pickDirectory: async () => '/tmp/project' })
expect((await selected.api.host.pickDirectory(request({}), new AbortController().signal)).result)
.toEqual({ ok: true, value: { path: '/tmp/project' } })
const cancelled = await harness(undefined, async () => null)
const cancelled = await harness(undefined, { pickDirectory: async () => null })
expect((await cancelled.api.host.pickDirectory(request({}), new AbortController().signal)).result)
.toEqual({ ok: true, value: { path: null } })
})
it('propagates abort into the native boundary as a cancelled RPC error', async () => {
const { api } = await harness(undefined, signal => new Promise((_resolve, reject) => {
signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
}))
const { api } = await harness(undefined, {
pickDirectory: signal => new Promise((_resolve, reject) => {
signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
}),
})
const abort = new AbortController()
const pending = api.host.pickDirectory(request({}), abort.signal)
abort.abort()
@@ -124,6 +130,30 @@ describe('host.pickDirectory', () => {
})
})
describe('host.openPath', () => {
it('opens through the injected native boundary', async () => {
const opened: string[] = []
const { api } = await harness(undefined, {
openPath: async (path) => { opened.push(path) },
})
expect((await api.host.openPath(request({ path: '/tmp/a.txt' }), new AbortController().signal)).result)
.toEqual({ ok: true, value: { opened: true } })
expect(opened).toEqual(['/tmp/a.txt'])
})
it('propagates abort into the native boundary as a cancelled RPC error', async () => {
const { api } = await harness(undefined, {
openPath: (_path, signal) => new Promise((_resolve, reject) => {
signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
}),
})
const abort = new AbortController()
const pending = api.host.openPath(request({ path: '/tmp/a.txt' }), abort.signal)
abort.abort()
expect((await pending).result).toMatchObject({ ok: false, error: { code: 'cancelled' } })
})
})
describe('workspace.create', () => {
it('serializes concurrent names and rejects the duplicate', async () => {
const { api, workspaceRoot } = await harness()

View File

@@ -50,6 +50,7 @@ function scriptedApi(overrides: {
host: {
describe: r => ok(r, { version: '0-test', cwd: '/t', attachedSessions: 0 }),
pickDirectory: r => ok(r, { path: null }),
openPath: r => ok(r, { opened: true as const }),
...overrides.host,
},
workspace: {

View File

@@ -80,6 +80,9 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
async pickDirectory(request) {
return { rpcId: request.rpcId, result: { ok: true, value: { path: null } } }
},
async openPath(request) {
return { rpcId: request.rpcId, result: { ok: true, value: { opened: true as const } } }
},
},
workspace: {
async list(request) {

View File

@@ -0,0 +1,62 @@
type ExecFileCallback = (
error: (Error & { code?: string | number }) | null,
stdout: string,
stderr: string,
) => void
type ExecFileMock = (
command: string,
args: readonly string[],
options: { encoding: string; signal: AbortSignal; windowsHide: boolean },
callback: ExecFileCallback,
) => void
const { execFileMock } = vi.hoisted(() => ({ execFileMock: vi.fn<ExecFileMock>() }))
vi.mock('node:child_process', () => ({ execFile: execFileMock }))
import { describe, expect, it, vi } from 'vitest'
import { openNativePath, type PathOpenerRunner } from '../src/native-path-opener.ts'
const signal = () => new AbortController().signal
describe('native path opener', () => {
it('opens with macOS open(1)', async () => {
const run = vi.fn<PathOpenerRunner>(async () => ({ stdout: '', stderr: '' }))
await openNativePath('/Users/test/file.txt', signal(), { platform: 'darwin', run })
expect(run).toHaveBeenCalledWith('open', ['/Users/test/file.txt'], expect.any(AbortSignal))
})
it('opens with Windows Invoke-Item and escapes single quotes', async () => {
const run = vi.fn<PathOpenerRunner>(async () => ({ stdout: '', stderr: '' }))
await openNativePath("C:\\work\\o'reilly.txt", signal(), { platform: 'win32', run })
expect(run).toHaveBeenCalledWith(
'powershell.exe',
['-NoProfile', '-Command', "Invoke-Item -LiteralPath 'C:\\work\\o''reilly.txt'"],
expect.any(AbortSignal),
)
})
it('opens with Linux xdg-open', async () => {
const run = vi.fn<PathOpenerRunner>(async () => ({ stdout: '', stderr: '' }))
await openNativePath('/tmp/a.txt', signal(), { platform: 'linux', run })
expect(run).toHaveBeenCalledWith('xdg-open', ['/tmp/a.txt'], expect.any(AbortSignal))
})
it('rejects unsupported platforms', async () => {
await expect(openNativePath('/x', signal(), { platform: 'freebsd' as NodeJS.Platform }))
.rejects.toThrow('unsupported on freebsd')
})
it('runs the default command adapter without a shell', async () => {
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
callback(null, '', '')
})
await openNativePath('/tmp/default.txt', signal(), { platform: 'darwin' })
const [command, args, options] = execFileMock.mock.calls[0]!
expect(command).toBe('open')
expect(args).toEqual(['/tmp/default.txt'])
expect(options.encoding).toBe('utf8')
expect(options.windowsHide).toBe(true)
expect(options.signal).toBeInstanceOf(AbortSignal)
})
})