Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 47fbf94

Browse files
committed
feat(roo-import): add Roo→Zoo migration handoff import service
1 parent 7d100dc commit 47fbf94

9 files changed

Lines changed: 857 additions & 0 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import * as assert from "assert"
2+
import * as fs from "fs/promises"
3+
import * as os from "os"
4+
import * as path from "path"
5+
6+
import * as vscode from "vscode"
7+
8+
import { setDefaultSuiteTimeout } from "./test-utils"
9+
10+
function makeHandoff(overrides: Record<string, unknown> = {}) {
11+
return {
12+
schemaVersion: 1,
13+
createdAt: new Date().toISOString(),
14+
source: {
15+
extensionId: "RooVeterinaryInc.roo-cline",
16+
publisher: "RooVeterinaryInc",
17+
name: "roo-cline",
18+
version: "3.53.1",
19+
globalStoragePath: "",
20+
storageBasePath: "",
21+
},
22+
zoo: {
23+
repositoryUrl: "https://github.com/Zoo-Code-Org/Zoo-Code",
24+
announcementUrl: "",
25+
extensionId: "ZooCodeOrganization.zoo-code",
26+
},
27+
containsSecrets: false,
28+
globalSettings: {},
29+
vscodeConfiguration: {},
30+
copiedData: {},
31+
...overrides,
32+
}
33+
}
34+
35+
suite("Roo Import", function () {
36+
setDefaultSuiteTimeout(this)
37+
38+
let tmpDir: string
39+
40+
suiteSetup(async () => {
41+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "zoo-e2e-roo-import-"))
42+
})
43+
44+
suiteTeardown(async () => {
45+
await fs.rm(tmpDir, { recursive: true, force: true })
46+
})
47+
48+
test("importRooHandoff command is registered", async () => {
49+
const commands = await vscode.commands.getCommands(true)
50+
assert.ok(
51+
commands.includes("roo-cline.importRooHandoff"),
52+
"roo-cline.importRooHandoff should be a registered command",
53+
)
54+
})
55+
56+
test("imports a valid handoff and copies tasks", async () => {
57+
const api = globalThis.api
58+
const storagePath = api.storagePath
59+
60+
// Build a minimal handoff with one task directory.
61+
const handoffDir = path.join(tmpDir, "handoff")
62+
const tasksSource = path.join(handoffDir, "data", "tasks")
63+
const taskId = `e2e-test-task-${Date.now()}`
64+
await fs.mkdir(path.join(tasksSource, taskId), { recursive: true })
65+
await fs.writeFile(path.join(tasksSource, taskId, "history_item.json"), JSON.stringify({ id: taskId }))
66+
67+
const handoffPath = path.join(handoffDir, "handoff-v1.json")
68+
await fs.writeFile(handoffPath, JSON.stringify(makeHandoff({ copiedData: { tasks: "data/tasks" } })))
69+
70+
// Execute the command with an explicit path to skip the file picker.
71+
const result = await vscode.commands.executeCommand<{ success: boolean; tasksCopied?: number; error?: string }>(
72+
"roo-cline.importRooHandoff",
73+
handoffPath,
74+
)
75+
76+
assert.ok(result, "Command should return a result")
77+
assert.strictEqual(result.success, true, `Import should succeed (error: ${result.error})`)
78+
assert.strictEqual(result.tasksCopied, 1, "One task should have been copied")
79+
80+
// Verify the task directory was actually written to Zoo's storage.
81+
const copiedTaskFile = path.join(storagePath, "tasks", taskId, "history_item.json")
82+
const contents = JSON.parse(await fs.readFile(copiedTaskFile, "utf-8"))
83+
assert.strictEqual(contents.id, taskId, "Task file content should match what was exported")
84+
})
85+
86+
test("skips import when handoff was already imported", async () => {
87+
const handoffPath = path.join(tmpDir, "handoff-duplicate.json")
88+
const createdAt = new Date().toISOString()
89+
await fs.writeFile(handoffPath, JSON.stringify(makeHandoff({ createdAt })))
90+
91+
// First import.
92+
const first = await vscode.commands.executeCommand<{ success: boolean }>(
93+
"roo-cline.importRooHandoff",
94+
handoffPath,
95+
)
96+
assert.ok(first?.success, "First import should succeed")
97+
98+
// Second import of the same handoff should also succeed (command accepts an
99+
// explicit path so it doesn't check the "already imported" state — that check
100+
// only runs in the activation notice path). Verify the command at least runs
101+
// without throwing.
102+
const second = await vscode.commands.executeCommand<{ success: boolean }>(
103+
"roo-cline.importRooHandoff",
104+
handoffPath,
105+
)
106+
assert.ok(second?.success, "Re-running the command with the same file should still succeed")
107+
})
108+
})

packages/types/src/vscode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const commandIds = [
4141

4242
"setCustomStoragePath",
4343
"importSettings",
44+
"importRooHandoff",
4445

4546
"focusInput",
4647
"acceptInput",

src/__mocks__/vscode.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export const workspace = {
4242
onDidChangeWorkspaceFolders: () => mockDisposable,
4343
getConfiguration: () => ({
4444
get: (key, defaultValue) => defaultValue,
45+
inspect: () => ({ globalValue: undefined }),
46+
update: () => Promise.resolve(),
4547
}),
4648
createFileSystemWatcher: () => ({
4749
onDidCreate: () => mockDisposable,
@@ -150,6 +152,12 @@ export const CodeActionKind = {
150152
RefactorRewrite: { value: "refactor.rewrite" },
151153
}
152154

155+
export const ConfigurationTarget = {
156+
Global: 1,
157+
Workspace: 2,
158+
WorkspaceFolder: 3,
159+
}
160+
153161
export const EventEmitter = mockEventEmitter
154162

155163
export default {
@@ -171,4 +179,5 @@ export default {
171179
EventEmitter,
172180
CodeAction,
173181
CodeActionKind,
182+
ConfigurationTarget,
174183
}

src/activate/registerCommands.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { handleNewTask } from "./handleTask"
1313
import { CodeIndexManager } from "../services/code-index/manager"
1414
import { importSettingsWithFeedback } from "../core/config/importExport"
1515
import { MdmService } from "../services/mdm/MdmService"
16+
import { promptAndImportRooHandoff, importRooHandoffFromPath } from "../services/roo-import/RooImport"
1617
import { t } from "../i18n"
1718

1819
/**
@@ -144,6 +145,36 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
144145
filePath,
145146
)
146147
},
148+
importRooHandoff: async (handoffPath?: string) => {
149+
const visibleProvider = await ClineProvider.getInstance()
150+
if (!visibleProvider) {
151+
return undefined
152+
}
153+
154+
const importOptions = {
155+
context,
156+
providerSettingsManager: visibleProvider.providerSettingsManager,
157+
contextProxy: visibleProvider.contextProxy,
158+
customModesManager: visibleProvider.customModesManager,
159+
outputChannel,
160+
}
161+
162+
try {
163+
const result = handoffPath
164+
? await importRooHandoffFromPath(handoffPath, importOptions)
165+
: await promptAndImportRooHandoff(importOptions)
166+
167+
if (result) {
168+
await visibleProvider.postStateToWebview()
169+
}
170+
171+
return result
172+
} catch (error) {
173+
const message = error instanceof Error ? error.message : String(error)
174+
outputChannel.appendLine(`[Roo Import] Failed: ${message}`)
175+
return undefined
176+
}
177+
},
147178
focusInput: async () => {
148179
try {
149180
await focusPanel(tabPanel, sidebarPanel)

src/extension.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { CodeIndexManager } from "./services/code-index/manager"
3838
import { MdmService } from "./services/mdm/MdmService"
3939
import { migrateSettings } from "./utils/migrateSettings"
4040
import { autoImportSettings } from "./utils/autoImportSettings"
41+
import { showRooHandoffNotice } from "./services/roo-import/RooImport"
4142
import { API } from "./extension/api"
4243

4344
import {
@@ -252,6 +253,18 @@ export async function activate(context: vscode.ExtensionContext) {
252253

253254
registerCommands({ context, outputChannel, provider })
254255

256+
void showRooHandoffNotice(context, {
257+
context,
258+
providerSettingsManager: provider.providerSettingsManager,
259+
contextProxy: provider.contextProxy,
260+
customModesManager: provider.customModesManager,
261+
outputChannel,
262+
}).catch((error) => {
263+
outputChannel.appendLine(
264+
`[Roo Import] Failed to check for handoff: ${error instanceof Error ? error.message : String(error)}`,
265+
)
266+
})
267+
255268
/**
256269
* We use the text document content provider API to show the left side for diff
257270
* view by creating a virtual document for the original content. This makes it

src/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@
150150
"title": "%command.importSettings.title%",
151151
"category": "%configuration.title%"
152152
},
153+
{
154+
"command": "roo-cline.importRooHandoff",
155+
"title": "%command.importRooHandoff.title%",
156+
"category": "%configuration.title%"
157+
},
153158
{
154159
"command": "roo-cline.focusInput",
155160
"title": "%command.focusInput.title%",

src/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"command.focusInput.title": "Focus Input Field",
2020
"command.setCustomStoragePath.title": "Set Custom Storage Path",
2121
"command.importSettings.title": "Import Settings",
22+
"command.importRooHandoff.title": "Import Roo Code Migration Handoff",
2223
"command.terminal.addToContext.title": "Add Terminal Content to Context",
2324
"command.terminal.fixCommand.title": "Fix This Command",
2425
"command.terminal.explainCommand.title": "Explain This Command",

0 commit comments

Comments
 (0)