mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Fix cli task resumption (#11739)
This commit is contained in:
parent
191053cec5
commit
efbba9e8d5
6 changed files with 171 additions and 7 deletions
|
|
@ -193,6 +193,7 @@ create_tarball() {
|
|||
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
|
@ -200,7 +201,10 @@ const __dirname = dirname(__filename);
|
|||
// Set environment variables for the CLI
|
||||
process.env.ROO_CLI_ROOT = join(__dirname, '..');
|
||||
process.env.ROO_EXTENSION_PATH = join(__dirname, '..', 'extension');
|
||||
process.env.ROO_RIPGREP_PATH = join(__dirname, 'rg');
|
||||
const ripgrepPath = join(__dirname, 'rg');
|
||||
if (existsSync(ripgrepPath)) {
|
||||
process.env.ROO_RIPGREP_PATH = ripgrepPath;
|
||||
}
|
||||
|
||||
// Import and run the actual CLI
|
||||
await import(join(__dirname, '..', 'lib', 'index.js'));
|
||||
|
|
@ -211,10 +215,21 @@ WRAPPER_EOF
|
|||
# Create empty .env file
|
||||
touch "$RELEASE_DIR/.env"
|
||||
|
||||
# Strip macOS metadata artifacts before packaging.
|
||||
find "$RELEASE_DIR" -type f -name "._*" -delete
|
||||
find "$RELEASE_DIR" -type f -name ".DS_Store" -delete
|
||||
find "$RELEASE_DIR" -type d -name "__MACOSX" -prune -exec rm -rf {} +
|
||||
|
||||
# Create tarball
|
||||
info "Creating tarball..."
|
||||
cd "$REPO_ROOT"
|
||||
tar -czvf "$TARBALL" "$(basename "$RELEASE_DIR")"
|
||||
COPYFILE_DISABLE=1 tar \
|
||||
--exclude="._*" \
|
||||
--exclude=".DS_Store" \
|
||||
--exclude="__MACOSX" \
|
||||
--exclude="*/._*" \
|
||||
--exclude="*/.DS_Store" \
|
||||
-czvf "$TARBALL" "$(basename "$RELEASE_DIR")"
|
||||
|
||||
# Clean up release directory
|
||||
rm -rf "$RELEASE_DIR"
|
||||
|
|
|
|||
|
|
@ -80,13 +80,28 @@ function spyOnPrivate(host: ExtensionHost, method: string) {
|
|||
}
|
||||
|
||||
describe("ExtensionHost", () => {
|
||||
const initialRooCliRuntimeEnv = process.env.ROO_CLI_RUNTIME
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
if (initialRooCliRuntimeEnv === undefined) {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
} else {
|
||||
process.env.ROO_CLI_RUNTIME = initialRooCliRuntimeEnv
|
||||
}
|
||||
// Clean up globals
|
||||
delete (global as Record<string, unknown>).vscode
|
||||
delete (global as Record<string, unknown>).__extensionHost
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (initialRooCliRuntimeEnv === undefined) {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
} else {
|
||||
process.env.ROO_CLI_RUNTIME = initialRooCliRuntimeEnv
|
||||
}
|
||||
})
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should store options correctly", () => {
|
||||
const options: ExtensionHostOptions = {
|
||||
|
|
@ -135,6 +150,12 @@ describe("ExtensionHost", () => {
|
|||
expect(getPrivate(host, "promptManager")).toBeDefined()
|
||||
expect(getPrivate(host, "askDispatcher")).toBeDefined()
|
||||
})
|
||||
|
||||
it("should mark process as CLI runtime", () => {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
createTestHost()
|
||||
expect(process.env.ROO_CLI_RUNTIME).toBe("1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("webview provider registration", () => {
|
||||
|
|
@ -429,6 +450,26 @@ describe("ExtensionHost", () => {
|
|||
|
||||
expect(restoreConsoleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should clear ROO_CLI_RUNTIME on dispose when it was previously unset", async () => {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
host = createTestHost()
|
||||
expect(process.env.ROO_CLI_RUNTIME).toBe("1")
|
||||
|
||||
await host.dispose()
|
||||
|
||||
expect(process.env.ROO_CLI_RUNTIME).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should restore prior ROO_CLI_RUNTIME value on dispose", async () => {
|
||||
process.env.ROO_CLI_RUNTIME = "preexisting-value"
|
||||
host = createTestHost()
|
||||
expect(process.env.ROO_CLI_RUNTIME).toBe("1")
|
||||
|
||||
await host.dispose()
|
||||
|
||||
expect(process.env.ROO_CLI_RUNTIME).toBe("preexisting-value")
|
||||
})
|
||||
})
|
||||
|
||||
describe("runTask", () => {
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
|
||||
// Ephemeral storage.
|
||||
private ephemeralStorageDir: string | null = null
|
||||
private previousCliRuntimeEnv: string | undefined
|
||||
|
||||
// ==========================================================================
|
||||
// Managers - These do all the heavy lifting
|
||||
|
|
@ -172,6 +173,10 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
super()
|
||||
|
||||
this.options = options
|
||||
// Mark this process as CLI runtime so extension code can apply
|
||||
// CLI-specific behavior without affecting VS Code desktop usage.
|
||||
this.previousCliRuntimeEnv = process.env.ROO_CLI_RUNTIME
|
||||
process.env.ROO_CLI_RUNTIME = "1"
|
||||
|
||||
// Enable file-based debug logging only when --debug is passed.
|
||||
if (options.debug) {
|
||||
|
|
@ -570,5 +575,12 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
// Restore previous CLI runtime marker for process hygiene in tests.
|
||||
if (this.previousCliRuntimeEnv === undefined) {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
} else {
|
||||
process.env.ROO_CLI_RUNTIME = this.previousCliRuntimeEnv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -963,6 +963,12 @@ export class ClineProvider
|
|||
historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task },
|
||||
options?: { startTask?: boolean },
|
||||
) {
|
||||
const isCliRuntime = process.env.ROO_CLI_RUNTIME === "1"
|
||||
// CLI injects runtime provider settings from command flags/env at startup.
|
||||
// Restoring provider profiles from task history can overwrite those
|
||||
// runtime settings with stale/incomplete persisted profiles.
|
||||
const skipProfileRestoreFromHistory = isCliRuntime
|
||||
|
||||
// Check if we're rehydrating the current task to avoid flicker
|
||||
const currentTask = this.getCurrentTask()
|
||||
const isRehydratingCurrentTask = currentTask && currentTask.taskId === historyItem.id
|
||||
|
|
@ -991,7 +997,8 @@ export class ClineProvider
|
|||
// Skip mode-based profile activation if historyItem.apiConfigName exists,
|
||||
// since the task's specific provider profile will override it anyway.
|
||||
const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false)
|
||||
if (!historyItem.apiConfigName && !lockApiConfigAcrossModes) {
|
||||
|
||||
if (!historyItem.apiConfigName && !lockApiConfigAcrossModes && !skipProfileRestoreFromHistory) {
|
||||
const savedConfigId = await this.providerSettingsManager.getModeConfigId(historyItem.mode)
|
||||
const listApiConfig = await this.providerSettingsManager.listConfig()
|
||||
|
||||
|
|
@ -1033,7 +1040,7 @@ export class ClineProvider
|
|||
// If the history item has a saved API config name (provider profile), restore it.
|
||||
// This overrides any mode-based config restoration above, because the task's
|
||||
// specific provider profile takes precedence over mode defaults.
|
||||
if (historyItem.apiConfigName) {
|
||||
if (historyItem.apiConfigName && !skipProfileRestoreFromHistory) {
|
||||
const listApiConfig = await this.providerSettingsManager.listConfig()
|
||||
// Keep global state/UI in sync with latest profiles for parity with mode restoration above.
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
|
|
@ -1059,6 +1066,10 @@ export class ClineProvider
|
|||
`Provider profile '${historyItem.apiConfigName}' from history no longer exists. Using current configuration.`,
|
||||
)
|
||||
}
|
||||
} else if (historyItem.apiConfigName && skipProfileRestoreFromHistory) {
|
||||
this.log(
|
||||
`Skipping restore of provider profile '${historyItem.apiConfigName}' for task ${historyItem.id} in CLI runtime.`,
|
||||
)
|
||||
}
|
||||
|
||||
const { apiConfiguration, enableCheckpoints, checkpointTimeout, experiments, cloudUserInfo, taskSyncEnabled } =
|
||||
|
|
|
|||
|
|
@ -201,10 +201,13 @@ describe("ClineProvider - Sticky Provider Profile", () => {
|
|||
let mockOutputChannel: vscode.OutputChannel
|
||||
let mockWebviewView: vscode.WebviewView
|
||||
let mockPostMessage: any
|
||||
let originalRooCliRuntimeEnv: string | undefined
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
taskIdCounter = 0
|
||||
originalRooCliRuntimeEnv = process.env.ROO_CLI_RUNTIME
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
|
||||
if (!TelemetryService.hasInstance()) {
|
||||
TelemetryService.createInstance([])
|
||||
|
|
@ -293,6 +296,14 @@ describe("ClineProvider - Sticky Provider Profile", () => {
|
|||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalRooCliRuntimeEnv === undefined) {
|
||||
delete process.env.ROO_CLI_RUNTIME
|
||||
} else {
|
||||
process.env.ROO_CLI_RUNTIME = originalRooCliRuntimeEnv
|
||||
}
|
||||
})
|
||||
|
||||
describe("activateProviderProfile", () => {
|
||||
beforeEach(async () => {
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
|
@ -457,7 +468,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
|
|||
})
|
||||
|
||||
describe("createTaskWithHistoryItem", () => {
|
||||
it("should restore provider profile from history item when reopening task", async () => {
|
||||
it("should restore provider profile from history item when reopening task outside CLI runtime", async () => {
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
// Create a history item with saved provider profile
|
||||
|
|
@ -495,6 +506,71 @@ describe("ClineProvider - Sticky Provider Profile", () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("should skip restoring task apiConfigName from history in CLI runtime", async () => {
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
process.env.ROO_CLI_RUNTIME = "1"
|
||||
|
||||
const historyItem: HistoryItem = {
|
||||
id: "test-task-id",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 200,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
totalCost: 0.001,
|
||||
apiConfigName: "saved-profile",
|
||||
}
|
||||
|
||||
const activateProviderProfileSpy = vi
|
||||
.spyOn(provider, "activateProviderProfile")
|
||||
.mockResolvedValue(undefined)
|
||||
const logSpy = vi.spyOn(provider, "log")
|
||||
|
||||
vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([
|
||||
{ name: "saved-profile", id: "saved-profile-id", apiProvider: "anthropic" },
|
||||
])
|
||||
|
||||
await provider.createTaskWithHistoryItem(historyItem)
|
||||
|
||||
expect(activateProviderProfileSpy).not.toHaveBeenCalledWith({ name: "saved-profile" }, expect.anything())
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Skipping restore of provider profile 'saved-profile'"),
|
||||
)
|
||||
})
|
||||
|
||||
it("should skip restoring mode-based provider config from history in CLI runtime", async () => {
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
process.env.ROO_CLI_RUNTIME = "1"
|
||||
|
||||
const historyItem: HistoryItem = {
|
||||
id: "test-task-id",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 200,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
totalCost: 0.001,
|
||||
mode: "code",
|
||||
}
|
||||
|
||||
const activateProviderProfileSpy = vi
|
||||
.spyOn(provider, "activateProviderProfile")
|
||||
.mockResolvedValue(undefined)
|
||||
|
||||
vi.spyOn(provider.providerSettingsManager, "getModeConfigId").mockResolvedValue("mode-config-id")
|
||||
vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([
|
||||
{ name: "mode-profile", id: "mode-config-id", apiProvider: "anthropic" },
|
||||
])
|
||||
|
||||
await provider.createTaskWithHistoryItem(historyItem)
|
||||
|
||||
expect(activateProviderProfileSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should use current profile if history item has no saved apiConfigName", async () => {
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ if (!isTestEnv) {
|
|||
const languageDirs = fs.readdirSync(localesDir, { withFileTypes: true })
|
||||
|
||||
const languages = languageDirs
|
||||
.filter((dirent: { isDirectory: () => boolean }) => dirent.isDirectory())
|
||||
.filter(
|
||||
(dirent: { isDirectory: () => boolean; name: string }) =>
|
||||
dirent.isDirectory() && !dirent.name.startsWith("."),
|
||||
)
|
||||
.map((dirent: { name: string }) => dirent.name)
|
||||
|
||||
// Process each language
|
||||
|
|
@ -28,7 +31,13 @@ if (!isTestEnv) {
|
|||
const langPath = path.join(localesDir, language)
|
||||
|
||||
// Find all JSON files in the language directory
|
||||
const files = fs.readdirSync(langPath).filter((file: string) => file.endsWith(".json"))
|
||||
const files = fs
|
||||
.readdirSync(langPath, { withFileTypes: true })
|
||||
.filter(
|
||||
(dirent: { isFile: () => boolean; name: string }) =>
|
||||
dirent.isFile() && dirent.name.endsWith(".json") && !dirent.name.startsWith("."),
|
||||
)
|
||||
.map((dirent: { name: string }) => dirent.name)
|
||||
|
||||
// Initialize language in translations object
|
||||
if (!translations[language]) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue