mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-12 23:01:21 +00:00
refactor: unify export path logic and default to Downloads (#10882)
This commit is contained in:
parent
3f332d8e2b
commit
5e0bb5af26
8 changed files with 143 additions and 38 deletions
|
|
@ -197,6 +197,9 @@ export const globalSettingsSchema = z.object({
|
|||
hasOpenedModeSelector: z.boolean().optional(),
|
||||
lastModeExportPath: z.string().optional(),
|
||||
lastModeImportPath: z.string().optional(),
|
||||
lastSettingsExportPath: z.string().optional(),
|
||||
lastTaskExportPath: z.string().optional(),
|
||||
lastImageSavePath: z.string().optional(),
|
||||
|
||||
/**
|
||||
* Path to worktree to auto-open after switching workspaces.
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ describe("importExport", () => {
|
|||
setValue: vi.fn(),
|
||||
export: vi.fn().mockImplementation(() => Promise.resolve({})),
|
||||
setProviderSettings: vi.fn(),
|
||||
getValue: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<ContextProxy>>
|
||||
|
||||
mockCustomModesManager = { updateCustomMode: vi.fn() } as unknown as ReturnType<
|
||||
|
|
@ -693,7 +694,7 @@ describe("importExport", () => {
|
|||
defaultUri: expect.anything(),
|
||||
})
|
||||
|
||||
expect(vscode.Uri.file).toHaveBeenCalledWith(path.join("/mock/home", "Documents", "roo-code-settings.json"))
|
||||
expect(vscode.Uri.file).toHaveBeenCalledWith(path.join("/mock/home", "Downloads", "roo-code-settings.json"))
|
||||
})
|
||||
|
||||
describe("codebase indexing export", () => {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { TelemetryService } from "@roo-code/telemetry"
|
|||
import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager"
|
||||
import { ContextProxy } from "./ContextProxy"
|
||||
import { CustomModesManager } from "./CustomModesManager"
|
||||
import { resolveDefaultSaveUri, saveLastExportPath } from "../../utils/export"
|
||||
import { t } from "../../i18n"
|
||||
|
||||
export type ImportOptions = {
|
||||
|
|
@ -143,15 +144,22 @@ export const importSettingsFromFile = async (
|
|||
}
|
||||
|
||||
export const exportSettings = async ({ providerSettingsManager, contextProxy }: ExportOptions) => {
|
||||
const defaultUri = await resolveDefaultSaveUri(contextProxy, "lastSettingsExportPath", "roo-code-settings.json", {
|
||||
useWorkspace: false,
|
||||
fallbackDir: path.join(os.homedir(), "Downloads"),
|
||||
})
|
||||
|
||||
const uri = await vscode.window.showSaveDialog({
|
||||
filters: { JSON: ["json"] },
|
||||
defaultUri: vscode.Uri.file(path.join(os.homedir(), "Documents", "roo-code-settings.json")),
|
||||
defaultUri,
|
||||
})
|
||||
|
||||
if (!uri) {
|
||||
return
|
||||
}
|
||||
|
||||
await saveLastExportPath(contextProxy, "lastSettingsExportPath", uri)
|
||||
|
||||
try {
|
||||
const providerProfiles = await providerSettingsManager.export()
|
||||
const globalSettings = await contextProxy.export()
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels"
|
|||
import { ProfileValidator } from "../../shared/ProfileValidator"
|
||||
|
||||
import { Terminal } from "../../integrations/terminal/Terminal"
|
||||
import { downloadTask } from "../../integrations/misc/export-markdown"
|
||||
import { downloadTask, getTaskFileName } from "../../integrations/misc/export-markdown"
|
||||
import { resolveDefaultSaveUri, saveLastExportPath } from "../../utils/export"
|
||||
import { getTheme } from "../../integrations/theme/getTheme"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
|
||||
|
|
@ -1724,7 +1725,16 @@ export class ClineProvider
|
|||
|
||||
async exportTaskWithId(id: string) {
|
||||
const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
|
||||
await downloadTask(historyItem.ts, apiConversationHistory)
|
||||
const fileName = getTaskFileName(historyItem.ts)
|
||||
const defaultUri = await resolveDefaultSaveUri(this.contextProxy, "lastTaskExportPath", fileName, {
|
||||
useWorkspace: false,
|
||||
fallbackDir: path.join(os.homedir(), "Downloads"),
|
||||
})
|
||||
const saveUri = await downloadTask(historyItem.ts, apiConversationHistory, defaultUri)
|
||||
|
||||
if (saveUri) {
|
||||
await saveLastExportPath(this.contextProxy, "lastTaskExportPath", saveUri)
|
||||
}
|
||||
}
|
||||
|
||||
/* Condenses a task's message history to use fewer tokens. */
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import { Mode, defaultModeSlug } from "../../shared/modes"
|
|||
import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
|
||||
import { GetModelsOptions } from "../../shared/api"
|
||||
import { generateSystemPrompt } from "./generateSystemPrompt"
|
||||
import { resolveDefaultSaveUri, saveLastExportPath } from "../../utils/export"
|
||||
import { getCommand } from "../../utils/commands"
|
||||
|
||||
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
|
||||
|
|
@ -1140,7 +1141,32 @@ export const webviewMessageHandler = async (
|
|||
openImage(message.text!, { values: message.values })
|
||||
break
|
||||
case "saveImage":
|
||||
saveImage(message.dataUri!)
|
||||
if (message.dataUri) {
|
||||
const matches = message.dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
|
||||
if (!matches) {
|
||||
// Let saveImage handle invalid URI error
|
||||
saveImage(message.dataUri, vscode.Uri.file(""))
|
||||
break
|
||||
}
|
||||
const format = matches[1]
|
||||
const defaultFileName = `img_${Date.now()}.${format}`
|
||||
|
||||
const defaultUri = await resolveDefaultSaveUri(
|
||||
provider.contextProxy,
|
||||
"lastImageSavePath",
|
||||
defaultFileName,
|
||||
{
|
||||
useWorkspace: false,
|
||||
fallbackDir: path.join(os.homedir(), "Downloads"),
|
||||
},
|
||||
)
|
||||
|
||||
const savedUri = await saveImage(message.dataUri, defaultUri)
|
||||
|
||||
if (savedUri) {
|
||||
await saveLastExportPath(provider.contextProxy, "lastImageSavePath", savedUri)
|
||||
}
|
||||
}
|
||||
break
|
||||
case "openFile":
|
||||
let filePath: string = message.text!
|
||||
|
|
@ -2122,25 +2148,15 @@ export const webviewMessageHandler = async (
|
|||
const result = await provider.customModesManager.exportModeWithRules(message.slug, customPrompt)
|
||||
|
||||
if (result.success && result.yaml) {
|
||||
// Get last used directory for export
|
||||
const lastExportPath = getGlobalState("lastModeExportPath")
|
||||
let defaultUri: vscode.Uri
|
||||
|
||||
if (lastExportPath) {
|
||||
// Use the directory from the last export
|
||||
const lastDir = path.dirname(lastExportPath)
|
||||
defaultUri = vscode.Uri.file(path.join(lastDir, `${message.slug}-export.yaml`))
|
||||
} else {
|
||||
// Default to workspace or home directory
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
defaultUri = vscode.Uri.file(
|
||||
path.join(workspaceFolders[0].uri.fsPath, `${message.slug}-export.yaml`),
|
||||
)
|
||||
} else {
|
||||
defaultUri = vscode.Uri.file(`${message.slug}-export.yaml`)
|
||||
}
|
||||
}
|
||||
const defaultUri = await resolveDefaultSaveUri(
|
||||
provider.contextProxy,
|
||||
"lastModeExportPath",
|
||||
`${message.slug}-export.yaml`,
|
||||
{
|
||||
useWorkspace: true,
|
||||
fallbackDir: path.join(os.homedir(), "Downloads"),
|
||||
},
|
||||
)
|
||||
|
||||
// Show save dialog
|
||||
const saveUri = await vscode.window.showSaveDialog({
|
||||
|
|
@ -2153,7 +2169,7 @@ export const webviewMessageHandler = async (
|
|||
|
||||
if (saveUri && result.yaml) {
|
||||
// Save the directory for next time
|
||||
await updateGlobalState("lastModeExportPath", saveUri.fsPath)
|
||||
await saveLastExportPath(provider.contextProxy, "lastModeExportPath", saveUri)
|
||||
|
||||
// Write the file to the selected location
|
||||
await fs.writeFile(saveUri.fsPath, result.yaml, "utf-8")
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ interface ReasoningBlock {
|
|||
|
||||
type ExtendedContentBlock = Anthropic.Messages.ContentBlockParam | ReasoningBlock
|
||||
|
||||
export async function downloadTask(dateTs: number, conversationHistory: Anthropic.MessageParam[]) {
|
||||
// File name
|
||||
export function getTaskFileName(dateTs: number): string {
|
||||
const date = new Date(dateTs)
|
||||
const month = date.toLocaleString("en-US", { month: "short" }).toLowerCase()
|
||||
const day = date.getDate()
|
||||
|
|
@ -23,7 +22,16 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
|
|||
const ampm = hours >= 12 ? "pm" : "am"
|
||||
hours = hours % 12
|
||||
hours = hours ? hours : 12 // the hour '0' should be '12'
|
||||
const fileName = `roo_task_${month}-${day}-${year}_${hours}-${minutes}-${seconds}-${ampm}.md`
|
||||
return `roo_task_${month}-${day}-${year}_${hours}-${minutes}-${seconds}-${ampm}.md`
|
||||
}
|
||||
|
||||
export async function downloadTask(
|
||||
dateTs: number,
|
||||
conversationHistory: Anthropic.MessageParam[],
|
||||
defaultUri: vscode.Uri,
|
||||
): Promise<vscode.Uri | undefined> {
|
||||
// File name
|
||||
const fileName = getTaskFileName(dateTs)
|
||||
|
||||
// Generate markdown
|
||||
const markdownContent = conversationHistory
|
||||
|
|
@ -39,14 +47,16 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
|
|||
// Prompt user for save location
|
||||
const saveUri = await vscode.window.showSaveDialog({
|
||||
filters: { Markdown: ["md"] },
|
||||
defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", fileName)),
|
||||
defaultUri,
|
||||
})
|
||||
|
||||
if (saveUri) {
|
||||
// Write content to the selected location
|
||||
await vscode.workspace.fs.writeFile(saveUri, Buffer.from(markdownContent))
|
||||
vscode.window.showTextDocument(saveUri, { preview: true })
|
||||
return saveUri
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function formatContentBlockToMarkdown(block: ExtendedContentBlock): string {
|
||||
|
|
|
|||
|
|
@ -90,21 +90,15 @@ export async function openImage(dataUriOrPath: string, options?: { values?: { ac
|
|||
}
|
||||
}
|
||||
|
||||
export async function saveImage(dataUri: string) {
|
||||
export async function saveImage(dataUri: string, defaultUri: vscode.Uri): Promise<vscode.Uri | undefined> {
|
||||
const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
|
||||
if (!matches) {
|
||||
vscode.window.showErrorMessage(t("common:errors.invalid_data_uri"))
|
||||
return
|
||||
return undefined
|
||||
}
|
||||
const [, format, base64Data] = matches
|
||||
const imageBuffer = Buffer.from(base64Data, "base64")
|
||||
|
||||
// Get workspace path or fallback to home directory
|
||||
const workspacePath = getWorkspacePath()
|
||||
const defaultPath = workspacePath || os.homedir()
|
||||
const defaultFileName = `img_${Date.now()}.${format}`
|
||||
const defaultUri = vscode.Uri.file(path.join(defaultPath, defaultFileName))
|
||||
|
||||
// Show save dialog
|
||||
const saveUri = await vscode.window.showSaveDialog({
|
||||
filters: {
|
||||
|
|
@ -116,15 +110,17 @@ export async function saveImage(dataUri: string) {
|
|||
|
||||
if (!saveUri) {
|
||||
// User cancelled the save dialog
|
||||
return
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
// Write the image to the selected location
|
||||
await vscode.workspace.fs.writeFile(saveUri, imageBuffer)
|
||||
vscode.window.showInformationMessage(t("common:info.image_saved", { path: saveUri.fsPath }))
|
||||
return saveUri
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
vscode.window.showErrorMessage(t("common:errors.error_saving_image", { errorMessage }))
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
|||
61
src/utils/export.ts
Normal file
61
src/utils/export.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
|
||||
export interface ExportContext {
|
||||
getValue(key: string): any
|
||||
setValue(key: string, value: any): Promise<void>
|
||||
}
|
||||
|
||||
export interface ExportOptions {
|
||||
/**
|
||||
* Whether to consider the active workspace folder as a default location.
|
||||
* Default: true
|
||||
*/
|
||||
useWorkspace?: boolean
|
||||
/**
|
||||
* Fallback directory if no previous path or workspace is available.
|
||||
*/
|
||||
fallbackDir?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the default save URI for an export operation.
|
||||
* Priorities:
|
||||
* 1. Last used export path (if available)
|
||||
* 2. Active workspace folder (if useWorkspace is true)
|
||||
* 3. Fallback directory (e.g. Downloads or Documents)
|
||||
* 4. Default to just the filename (user's home/cwd)
|
||||
*/
|
||||
export function resolveDefaultSaveUri(
|
||||
context: ExportContext,
|
||||
configKey: string,
|
||||
fileName: string,
|
||||
options: ExportOptions = {},
|
||||
): vscode.Uri {
|
||||
const { useWorkspace = true, fallbackDir } = options
|
||||
const lastExportPath = context.getValue(configKey) as string | undefined
|
||||
|
||||
if (lastExportPath) {
|
||||
// Use the directory from the last export
|
||||
const lastDir = path.dirname(lastExportPath)
|
||||
return vscode.Uri.file(path.join(lastDir, fileName))
|
||||
} else {
|
||||
// Try workspace if enabled
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (useWorkspace && workspaceFolders && workspaceFolders.length > 0) {
|
||||
return vscode.Uri.file(path.join(workspaceFolders[0].uri.fsPath, fileName))
|
||||
}
|
||||
|
||||
// Fallback
|
||||
if (fallbackDir) {
|
||||
return vscode.Uri.file(path.join(fallbackDir, fileName))
|
||||
}
|
||||
|
||||
// Default to cwd/home
|
||||
return vscode.Uri.file(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveLastExportPath(context: ExportContext, configKey: string, uri: vscode.Uri) {
|
||||
await context.setValue(configKey, uri.fsPath)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue