✨ feat(vscode): 移交包不带子代理对话

This commit is contained in:
cruldra committed 2026-09-27 18:46:38 +08:00
1 parent a5ea0ddb57
commit c86939285d
6 files changed
+33 -15

No files matched your search

+9 -5
View File
@@ -31,9 +31,11 @@ function senderRoots(): { projectsRoot: string, codexRoot: string } {
const wt = path.join(projectsRoot, '-Users-chw-wt-x')
mkdirSync(proj, { recursive: true })
mkdirSync(path.join(wt, IMPL, 'subagents'), { recursive: true })
mkdirSync(path.join(wt, IMPL, 'tool-results'), { recursive: true })
writeFileSync(path.join(proj, `${BRAIN}.jsonl`), 'brain\n')
writeFileSync(path.join(wt, `${IMPL}.jsonl`), 'impl\n')
writeFileSync(path.join(wt, IMPL, 'subagents', 'agent-1.jsonl'), '{}\n')
writeFileSync(path.join(wt, IMPL, 'tool-results', 'out.txt'), 'tool\n')
const day = path.join(codexRoot, '2026', '08', '26')
mkdirSync(day, { recursive: true })
writeFileSync(path.join(day, `rollout-2026-08-26T01-02-03-${REVIEW}.jsonl`), 'review\n')
@@ -56,7 +58,8 @@ describe('stageHandoffBundle', () => {
expect(manifest.sessions.testSessionId).toBe('missing-0000-4000-8000-000000000000')
expect(manifest.createdAt).toBe('2026-08-26T00:00:00.000Z')
expect(existsSync(path.join(staging, 'claude', `${IMPL}.jsonl`))).toBe(true)
expect(existsSync(path.join(staging, 'claude', IMPL, 'subagents', 'agent-1.jsonl'))).toBe(true)
expect(existsSync(path.join(staging, 'claude', IMPL, 'subagents', 'agent-1.jsonl'))).toBe(false)
expect(existsSync(path.join(staging, 'claude', IMPL, 'tool-results', 'out.txt'))).toBe(true)
expect(existsSync(path.join(staging, 'codex', manifest.codex[0].relPath))).toBe(true)
expect(parseHandoffManifest(readFileSync(path.join(staging, HANDOFF_MANIFEST_FILE), 'utf8'), 7)).toEqual(manifest)
})
@@ -92,7 +95,8 @@ describe('pack → unpack → install', () => {
await installHandoffBundle(extracted, parsed, { projectsRoot, codexRoot, workspaceProjectsDir, worktreeProjectsDir })
expect(readFileSync(path.join(workspaceProjectsDir, `${BRAIN}.jsonl`), 'utf8')).toBe('brain\n')
expect(readFileSync(path.join(worktreeProjectsDir, `${IMPL}.jsonl`), 'utf8')).toBe('impl\n')
expect(existsSync(path.join(worktreeProjectsDir, IMPL, 'subagents', 'agent-1.jsonl'))).toBe(true)
expect(existsSync(path.join(worktreeProjectsDir, IMPL, 'subagents', 'agent-1.jsonl'))).toBe(false)
expect(readFileSync(path.join(worktreeProjectsDir, IMPL, 'tool-results', 'out.txt'), 'utf8')).toBe('tool\n')
expect(existsSync(path.join(projectsRoot, '-stale', `${IMPL}.jsonl`))).toBe(false)
expect(readFileSync(path.join(codexRoot, parsed.codex[0].relPath), 'utf8')).toBe('review\n')
})
@@ -114,7 +118,7 @@ describe('pack → unpack → install', () => {
// sender 会话文件里嵌入发送方的 worktree 路径(plain / file:// 两种形式)
const wt = path.join(roots.projectsRoot, '-Users-chw-wt-x')
writeFileSync(path.join(wt, `${IMPL}.jsonl`), `{"cwd":"${WORKTREE_FROM}"}\n{"cwd":"file://${WORKTREE_FROM}"}\n`)
writeFileSync(path.join(wt, IMPL, 'subagents', 'agent-1.jsonl'), `{"cwd":"${WORKTREE_FROM}"}\n`)
writeFileSync(path.join(wt, IMPL, 'tool-results', 'out.txt'), `{"cwd":"${WORKTREE_FROM}"}\n`)
const day = path.join(roots.codexRoot, '2026', '08', '26')
writeFileSync(path.join(day, `rollout-2026-08-26T01-02-03-${REVIEW}.jsonl`), `{"cwd":"${WORKTREE_FROM}"}\n{"cwd":"file://${WORKTREE_FROM}"}\n`)
@@ -140,9 +144,9 @@ describe('pack → unpack → install', () => {
})
const installedJsonl = readFileSync(path.join(worktreeProjectsDir, `${IMPL}.jsonl`), 'utf8')
const installedSubagent = readFileSync(path.join(worktreeProjectsDir, IMPL, 'subagents', 'agent-1.jsonl'), 'utf8')
const installedTool = readFileSync(path.join(worktreeProjectsDir, IMPL, 'tool-results', 'out.txt'), 'utf8')
const installedRollout = readFileSync(path.join(codexRoot, manifest.codex[0].relPath), 'utf8')
for (const content of [installedJsonl, installedSubagent, installedRollout]) {
for (const content of [installedJsonl, installedTool, installedRollout]) {
expect(content).toContain(WORKTREE_TO)
expect(content).not.toContain(WORKTREE_FROM)
}
+15 -3
View File
@@ -1,8 +1,10 @@
import type { HandoffManifest, HandoffProfiles, HandoffSessions } from './handoffManifest'
import type { ClaudeSessionFiles } from './sessionBundle'
import { promises as fsp } from 'node:fs'
import * as path from 'node:path'
import { CLAUDE_SESSION_FIELDS, HANDOFF_MANIFEST_FILE } from './handoffManifest'
import {
codexRolloutRelPathFor,
findClaudeSessionFiles,
installClaudeSession,
@@ -40,7 +42,12 @@ async function placeFile(src: string, dst: string): Promise<void> {
}
}
/** 子代理对话。接手的人只恢复主会话,不打包。 */
export const HANDOFF_SKIPPED_DIR = 'subagents'
async function placeTree(src: string, dst: string): Promise<void> {
if (path.basename(src) === HANDOFF_SKIPPED_DIR)
return
const st = await fsp.lstat(src)
if (st.isDirectory()) {
await fsp.mkdir(dst, { recursive: true })
@@ -51,6 +58,13 @@ async function placeTree(src: string, dst: string): Promise<void> {
await placeFile(src, dst)
}
export async function stageClaudeSessionFiles(files: ClaudeSessionFiles, dstDir: string): Promise<void> {
await fsp.mkdir(dstDir, { recursive: true })
await placeFile(files.jsonl, path.join(dstDir, path.basename(files.jsonl)))
if (files.dir)
await placeTree(files.dir, path.join(dstDir, path.basename(files.dir)))
}
export async function stageHandoffBundle(
src: HandoffSource,
stagingDir: string,
@@ -70,9 +84,7 @@ export async function stageHandoffBundle(
const files = await findClaudeSessionFiles(id, roots.projectsRoot)
if (!files)
continue
await placeFile(files.jsonl, path.join(claudeDir, path.basename(files.jsonl)))
if (files.dir)
await placeTree(files.dir, path.join(claudeDir, path.basename(files.dir)))
await stageClaudeSessionFiles(files, claudeDir)
claude.push({ id, kind })
}
+1 -1
View File
@@ -72,7 +72,7 @@ describe('measureHandoffSources', () => {
const items = await measureHandoffSources(src, roots)
expect(items).toEqual([
{ role: '头脑风暴', bytes: Buffer.byteLength('brain\n') },
{ role: '实施', bytes: Buffer.byteLength('impl\n') + Buffer.byteLength('{}\n') },
{ role: '实施', bytes: Buffer.byteLength('impl\n') },
{ role: '审查', bytes: Buffer.byteLength('review\n') },
])
})
+5 -2
View File
@@ -3,7 +3,7 @@ import { promises as fsp } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { formatByteSize, HANDOFF_ATTACHMENT_MAX_BYTES, HandoffArchiveTooLargeError, packHandoff } from './handoffArchive'
import { stageHandoffBundle } from './handoffBundle'
import { HANDOFF_SKIPPED_DIR, stageHandoffBundle } from './handoffBundle'
import { CLAUDE_SESSION_FIELDS } from './handoffManifest'
import { codexRolloutRelPathFor, findClaudeSessionFiles } from './sessionBundle'
@@ -45,8 +45,11 @@ async function treeBytes(abs: string): Promise<number> {
if (!st.isDirectory())
return st.size
let total = 0
for (const name of await fsp.readdir(abs))
for (const name of await fsp.readdir(abs)) {
if (name === HANDOFF_SKIPPED_DIR)
continue
total += await treeBytes(path.join(abs, name))
}
return total
}