fix: complete subagent output and file workspace interactions
OpenCode child cards now show ordered text, reasoning and tool progress, with the final result displayed separately. Local SDK and remote ACP paths share child tracking, including progress retained across recovery and cancellation. Remote live progress requires an Agent package containing the event plugin; older Agent packages still expose final results without inventing missing history. Knowledge-library rows remain clickable when native label activation changes focus. File workspace changes use colored Git status letters and per-file staged/unstaged diffs, including deleted, renamed and untracked files. Refreshing reloads the selected diff and expanded directories, and more than 50 changes remain accessible through incremental loading. Validated with 3552 passing tests, typecheck, lint, desktop build, current-source Linux Host scenarios and isolated Windows desktop UI interactions. Release note: 展开 OpenCode 子代理卡片可查看执行过程、工具详情与独立最终结果;知识库支持整行勾选。工作区支持单文件暂存与未暂存差异,刷新会更新当前差异,更多变更可继续加载。远端实时过程需要更新 GoodBuddy Agent。
This commit is contained in:
@@ -775,53 +775,76 @@ export class LocalWorkspaceAccess implements WorkspaceAccess {
|
||||
canonicalRoot,
|
||||
input.signal
|
||||
)
|
||||
const [statusResult, patchResult] = await Promise.all([
|
||||
runGit(
|
||||
const git = (args: string[]): Promise<CommandResult> =>
|
||||
runGit(canonicalRoot, withSafeGitArguments(args), input.signal, filterOverrides)
|
||||
const statusResult = await runGit(
|
||||
canonicalRoot,
|
||||
withSafeGitArguments([
|
||||
'status',
|
||||
'--porcelain=v1',
|
||||
'-z',
|
||||
'--untracked-files=normal'
|
||||
]),
|
||||
input.signal,
|
||||
filterOverrides
|
||||
),
|
||||
runGit(
|
||||
canonicalRoot,
|
||||
withSafeGitArguments([
|
||||
'diff',
|
||||
'--no-ext-diff',
|
||||
'--no-textconv',
|
||||
'--no-color',
|
||||
'HEAD'
|
||||
'--untracked-files=all'
|
||||
]),
|
||||
input.signal,
|
||||
filterOverrides
|
||||
)
|
||||
])
|
||||
if (statusResult.code !== 0 || patchResult.code !== 0) {
|
||||
const detail = statusResult.stderr || patchResult.stderr
|
||||
if (statusResult.code !== 0) {
|
||||
const detail = statusResult.stderr
|
||||
return {
|
||||
rootPath: this.rootPath,
|
||||
available: false,
|
||||
status: '',
|
||||
patch: '',
|
||||
files: [],
|
||||
truncated: statusResult.truncated || patchResult.truncated,
|
||||
truncated: statusResult.truncated,
|
||||
error: detail.trim().slice(0, 2_000) || '无法读取 Git 工作区'
|
||||
}
|
||||
}
|
||||
const changedFiles = parseChangedFiles(statusResult.stdout)
|
||||
const diffArgs = ['diff', '--no-ext-diff', '--no-textconv', '--no-color']
|
||||
let patchResult: CommandResult
|
||||
let stagedResult: CommandResult | undefined
|
||||
if (input.path !== undefined) {
|
||||
const path = normalizeRelativePath(input.path, false).path
|
||||
const file = changedFiles.files.find((entry) => entry.path === path)
|
||||
if (!file) {
|
||||
throw new Error('File is no longer in the Git changes list')
|
||||
}
|
||||
const paths = [...new Set([file.previousPath, path].filter((value): value is string => value !== undefined))]
|
||||
.map((value) => `:(literal)${value}`)
|
||||
if (file.status === '??') {
|
||||
await this.resolveEntryPath(path, 'file')
|
||||
patchResult = await git([...diffArgs, '--no-index', '--', NULL_DEVICE, path])
|
||||
if (patchResult.code !== 0 && patchResult.code !== 1) {
|
||||
throw new Error(patchResult.stderr || 'Git diff failed')
|
||||
}
|
||||
} else {
|
||||
;[stagedResult, patchResult] = await Promise.all([
|
||||
git([...diffArgs, '--cached', '--', ...paths]),
|
||||
git([...diffArgs, '--', ...paths])
|
||||
])
|
||||
if (stagedResult.code !== 0 || patchResult.code !== 0) {
|
||||
throw new Error(stagedResult.stderr || patchResult.stderr || 'Git diff failed')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const head = await git(['rev-parse', '--verify', '--quiet', 'HEAD'])
|
||||
patchResult = await git([...diffArgs, ...(head.code === 0 ? ['HEAD'] : ['--cached'])])
|
||||
}
|
||||
return {
|
||||
rootPath: this.rootPath,
|
||||
available: true,
|
||||
status: formatChangedFiles(changedFiles.files),
|
||||
patch: patchResult.stdout,
|
||||
...(patchResult.code !== 0 && input.path === undefined
|
||||
? { error: patchResult.stderr.trim().slice(0, 2000) || 'Git diff failed' }
|
||||
: {}),
|
||||
...(stagedResult ? { stagedPatch: stagedResult.stdout } : {}),
|
||||
files: changedFiles.files,
|
||||
truncated:
|
||||
statusResult.truncated ||
|
||||
patchResult.truncated ||
|
||||
stagedResult?.truncated === true ||
|
||||
changedFiles.truncated
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -113,6 +113,26 @@ function createAccess(lease: RemoteWorkspaceTransportLease): {
|
||||
}
|
||||
|
||||
describe('RemoteWorkspaceAccess', () => {
|
||||
it('requests both single-file diff layers and preserves raw Git status', async () => {
|
||||
const lease = createLease({
|
||||
getGitStatus: vi.fn(async () => ({
|
||||
repositoryIdentity: 'repository-1', branch: 'main', truncated: false,
|
||||
entries: [{ relativePath: '[file].txt', index: 'modified' as const, worktree: 'unmodified' as const, statusCode: 'T.' }]
|
||||
})),
|
||||
getGitDiff: vi.fn(async (request) => ({
|
||||
repositoryIdentity: 'repository-1', patch: request.staged ? 'staged' : 'worktree', byteLength: request.staged ? 6 : 8, truncated: request.staged
|
||||
}))
|
||||
})
|
||||
const { access } = createAccess(lease)
|
||||
expect(await access.getChanges({ path: '[file].txt' })).toMatchObject({
|
||||
available: true, patch: 'worktree', stagedPatch: 'staged', truncated: true,
|
||||
files: [{ path: '[file].txt', status: 'T ' }]
|
||||
})
|
||||
expect(lease.getGitDiff).toHaveBeenCalledWith(expect.objectContaining({ relativePath: '[file].txt', staged: true }), undefined)
|
||||
expect(lease.getGitDiff).toHaveBeenCalledWith(expect.objectContaining({ relativePath: '[file].txt', staged: false }), undefined)
|
||||
expect(lease.statWorkspace).not.toHaveBeenCalled()
|
||||
await access.dispose()
|
||||
})
|
||||
it('lazily validates once and maps bounded workspace methods', async () => {
|
||||
const lease = createLease({
|
||||
listWorkspace: vi.fn(async () => ({
|
||||
|
||||
@@ -247,7 +247,7 @@ function mapChangedFiles(
|
||||
: `${gitStatusCharacter(entry.index)}${gitStatusCharacter(entry.worktree)}`
|
||||
return {
|
||||
path: entry.relativePath,
|
||||
status,
|
||||
status: entry.statusCode?.replaceAll('.', ' ') ?? status,
|
||||
...(entry.originalRelativePath
|
||||
? { previousPath: entry.originalRelativePath }
|
||||
: {})
|
||||
@@ -577,8 +577,7 @@ export class RemoteWorkspaceAccess implements WorkspaceAccess {
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
input.signal?.throwIfAborted()
|
||||
const reference = this.reference(opened)
|
||||
const [status, diff] = await Promise.all([
|
||||
opened.lease
|
||||
const status = await opened.lease
|
||||
.getGitStatus(
|
||||
remoteGitStatusRequestSchema.parse({
|
||||
...reference,
|
||||
@@ -590,12 +589,13 @@ export class RemoteWorkspaceAccess implements WorkspaceAccess {
|
||||
)
|
||||
.then((result) =>
|
||||
remoteGitStatusResultSchema.parse(result)
|
||||
),
|
||||
opened.lease
|
||||
)
|
||||
const readDiff = (staged: boolean) => opened.lease
|
||||
.getGitDiff(
|
||||
remoteGitDiffRequestSchema.parse({
|
||||
...reference,
|
||||
staged: false,
|
||||
staged,
|
||||
...(input.path === undefined ? {} : { relativePath: input.path }),
|
||||
maximumBytes:
|
||||
REMOTE_WORKSPACE_LIMITS.maximumGitDiffBytes
|
||||
}),
|
||||
@@ -604,8 +604,12 @@ export class RemoteWorkspaceAccess implements WorkspaceAccess {
|
||||
.then((result) =>
|
||||
remoteGitDiffResultSchema.parse(result)
|
||||
)
|
||||
const [diff, stagedDiff] = await Promise.all([
|
||||
readDiff(false),
|
||||
input.path === undefined ? undefined : readDiff(true)
|
||||
])
|
||||
if (status.repositoryIdentity !== diff.repositoryIdentity) {
|
||||
if (status.repositoryIdentity !== diff.repositoryIdentity ||
|
||||
(stagedDiff && status.repositoryIdentity !== stagedDiff.repositoryIdentity)) {
|
||||
if (attempt === 0) {
|
||||
continue
|
||||
}
|
||||
@@ -617,10 +621,12 @@ export class RemoteWorkspaceAccess implements WorkspaceAccess {
|
||||
available: true,
|
||||
status: formatChangedFiles(files),
|
||||
patch: diff.patch,
|
||||
...(stagedDiff ? { stagedPatch: stagedDiff.patch } : {}),
|
||||
files,
|
||||
truncated:
|
||||
status.truncated ||
|
||||
diff.truncated ||
|
||||
stagedDiff?.truncated === true ||
|
||||
diff.nextCursor !== undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export type SearchPage = {
|
||||
}
|
||||
|
||||
export type WorkspaceChangesInput = {
|
||||
path?: string
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user