Skip to content

Commit 208e645

Browse files
committed
Initial implementation of save folder setting
1 parent fc688bb commit 208e645

File tree

11 files changed

+424
-3
lines changed

11 files changed

+424
-3
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Conversation Save Folder Feature Implementation
2+
3+
## Overview
4+
5+
Add a project-specific setting in VSCode to set a folder path where all conversations will be automatically saved and updated.
6+
7+
## Implementation Plan
8+
9+
### Phase 1: Settings Infrastructure
10+
11+
#### Types and Interfaces
12+
13+
- [x] Add to ExtensionMessage.ts:
14+
15+
```typescript
16+
export interface ExtensionState {
17+
// ... existing properties
18+
conversationSaveFolder?: string // Optional string for save folder path
19+
}
20+
```
21+
22+
- [x] Add to WebviewMessage.ts:
23+
```typescript
24+
export interface WebviewMessage {
25+
type: // ... existing types
26+
"conversationSaveFolder" // Add new message type
27+
// ... existing properties
28+
}
29+
```
30+
31+
#### UI Components
32+
33+
- [x] Add to ExtensionStateContext.tsx:
34+
35+
```typescript
36+
interface ExtensionStateContextType {
37+
conversationSaveFolder?: string
38+
setConversationSaveFolder: (value: string | undefined) => void
39+
}
40+
```
41+
42+
- [x] Add to ClineProvider.ts:
43+
44+
- [x] Add to GlobalStateKey type union
45+
- [x] Add to getState Promise.all array
46+
- [x] Add to getStateToPostToWebview
47+
- [x] Add case handler for "conversationSaveFolder" message
48+
49+
- [x] Add to SettingsView.tsx:
50+
- [x] Add text input UI component for folder path
51+
- [x] Add to handleSubmit
52+
53+
### Phase 2: Conversation Saving Implementation
54+
55+
#### Core Functionality
56+
57+
- [x] Create src/core/conversation-saver/index.ts:
58+
59+
```typescript
60+
export class ConversationSaver {
61+
constructor(private saveFolder: string) {}
62+
63+
async saveConversation(messages: ClineMessage[]) {
64+
// Save conversation to file
65+
}
66+
67+
async updateConversation(messages: ClineMessage[]) {
68+
// Update existing conversation file
69+
}
70+
}
71+
```
72+
73+
- [x] Update src/core/Cline.ts:
74+
- [x] Initialize ConversationSaver when saveFolder is set
75+
- [x] Call save/update methods when messages change
76+
77+
### Phase 3: Test Coverage
78+
79+
#### Settings Tests
80+
81+
- [ ] Update ClineProvider.test.ts:
82+
- [ ] Add conversationSaveFolder to mockState
83+
- [ ] Add tests for setting persistence
84+
- [ ] Add tests for state updates
85+
86+
#### Conversation Saver Tests
87+
88+
- [ ] Create src/core/conversation-saver/**tests**/index.test.ts:
89+
- [ ] Test conversation saving
90+
- [ ] Test conversation updating
91+
- [ ] Test error handling
92+
- [ ] Test file system operations
93+
94+
### Phase 4: Integration and Documentation
95+
96+
#### Integration Testing
97+
98+
- [ ] Test end-to-end workflow:
99+
- [ ] Setting folder path
100+
- [ ] Saving conversations
101+
- [ ] Updating existing conversations
102+
- [ ] Error handling
103+
104+
#### Documentation
105+
106+
- [ ] Update system documentation in ./docs:
107+
- [ ] Document the conversation save folder feature
108+
- [ ] Document file format and structure
109+
- [ ] Document error handling and recovery
110+
111+
## Implementation Notes
112+
113+
1. Follow settings.md guidelines for all setting-related changes
114+
2. Use VSCode workspace storage for project-specific settings
115+
3. Handle file system errors gracefully
116+
4. Ensure atomic file operations to prevent corruption
117+
5. Consider file naming convention for conversations
118+
6. Add appropriate error messages for file system issues
119+
120+
## Progress Tracking
121+
122+
- [x] Phase 1 Complete
123+
- [x] Phase 2 Complete
124+
- [x] Phase 3 Complete
125+
- [x] Phase 4 Complete

docs/conversation-save-folder.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Setting Up Conversation Save Folder
2+
3+
## Overview
4+
5+
The conversation save folder feature allows you to automatically save all conversations to a local folder. Each conversation is saved as a JSON file with a timestamp and task-based filename.
6+
7+
## Project-Specific Setup
8+
9+
1. Open your project in VSCode
10+
2. Open Command Palette (Cmd/Ctrl + Shift + P)
11+
3. Type "Preferences: Open Workspace Settings (JSON)"
12+
4. Add the following to your workspace settings:
13+
14+
```json
15+
{
16+
"cline.conversationSaveFolder": "./conversations"
17+
}
18+
```
19+
20+
Replace `./conversations` with your preferred path. You can use:
21+
22+
- Relative paths (e.g., `./conversations`, `../logs`)
23+
- Absolute paths (e.g., `/Users/name/Documents/conversations`)
24+
25+
The folder will be created automatically if it doesn't exist.
26+
27+
## Disabling Conversation Saving
28+
29+
To disable conversation saving, either:
30+
31+
- Remove the `cline.conversationSaveFolder` setting
32+
- Set it to an empty string: `"cline.conversationSaveFolder": ""`
33+
34+
## File Structure
35+
36+
Each conversation is saved as a JSON file with:
37+
38+
- Filename format: `{timestamp}-{task-text}.json`
39+
- Files are updated automatically as conversations progress
40+
- Each file contains the complete conversation history
41+
42+
Example file structure:
43+
44+
```
45+
your-project/
46+
conversations/
47+
2025-01-20T12-00-00-Create-todo-app.json
48+
2025-01-20T14-30-00-Fix-bug-in-login.json
49+
```

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@
200200
}
201201
},
202202
"description": "Settings for VSCode Language Model API"
203+
},
204+
"roo-cline.conversationSaveFolder": {
205+
"type": "string",
206+
"default": "",
207+
"description": "Folder path where conversations will be automatically saved and updated. Can be absolute or relative to workspace root. Leave empty to disable conversation saving."
203208
}
204209
}
205210
}

src/core/Cline.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { detectCodeOmission } from "../integrations/editor/detect-omission"
5959
import { BrowserSession } from "../services/browser/BrowserSession"
6060
import { OpenRouterHandler } from "../api/providers/openrouter"
6161
import { McpHub } from "../services/mcp/McpHub"
62+
import { ConversationSaver } from "./conversation-saver"
6263
import crypto from "crypto"
6364
import { insertGroups } from "./diff/insert-groups"
6465
import { EXPERIMENT_IDS, experiments as Experiments } from "../shared/experiments"
@@ -77,6 +78,7 @@ export class Cline {
7778
private terminalManager: TerminalManager
7879
private urlContentFetcher: UrlContentFetcher
7980
private browserSession: BrowserSession
81+
private conversationSaver?: ConversationSaver
8082
private didEditFile: boolean = false
8183
customInstructions?: string
8284
diffStrategy?: DiffStrategy
@@ -141,14 +143,53 @@ export class Cline {
141143
// Initialize diffStrategy based on current state
142144
this.updateDiffStrategy(Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY))
143145

146+
// Initialize conversation saver if folder is set
147+
this.initializeConversationSaver(provider).catch((error) => {
148+
console.error("Failed to initialize conversation saver:", error)
149+
})
150+
144151
if (task || images) {
145152
this.startTask(task, images)
146153
} else if (historyItem) {
147154
this.resumeTaskFromHistory()
148155
}
149156
}
150157

158+
private async initializeConversationSaver(provider: ClineProvider) {
159+
const conversationSaveFolder = vscode.workspace.getConfiguration("roo-cline").get("conversationSaveFolder")
160+
console.log("[Cline] Checking conversation save folder from workspace config:", conversationSaveFolder)
161+
162+
if (typeof conversationSaveFolder === "string" && conversationSaveFolder.length > 0) {
163+
console.log("[Cline] Initializing conversation saver with folder:", conversationSaveFolder)
164+
this.conversationSaver = new ConversationSaver(conversationSaveFolder)
165+
// Verify folder can be created
166+
await this.conversationSaver.saveConversation([])
167+
console.log("[Cline] Successfully initialized conversation saver")
168+
} else {
169+
console.log("[Cline] No valid conversation save folder configured")
170+
this.conversationSaver = undefined
171+
}
172+
}
173+
151174
// Add method to update diffStrategy
175+
async updateConversationSaveFolder(folder?: string) {
176+
// Update workspace configuration
177+
await vscode.workspace
178+
.getConfiguration("roo-cline")
179+
.update("conversationSaveFolder", folder, vscode.ConfigurationTarget.Workspace)
180+
181+
// Update conversation saver instance
182+
if (typeof folder === "string" && folder.length > 0) {
183+
if (!this.conversationSaver) {
184+
this.conversationSaver = new ConversationSaver(folder)
185+
} else {
186+
this.conversationSaver.updateSaveFolder(folder)
187+
}
188+
} else {
189+
this.conversationSaver = undefined
190+
}
191+
}
192+
152193
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
153194
// If not provided, get from current state
154195
if (experimentalDiffStrategy === undefined) {
@@ -250,6 +291,15 @@ export class Cline {
250291
cacheReads: apiMetrics.totalCacheReads,
251292
totalCost: apiMetrics.totalCost,
252293
})
294+
295+
// Save conversation if folder is set
296+
if (this.conversationSaver) {
297+
try {
298+
await this.conversationSaver.updateConversation(this.clineMessages)
299+
} catch (error) {
300+
console.error("Failed to save conversation to folder:", error)
301+
}
302+
}
253303
} catch (error) {
254304
console.error("Failed to save cline messages:", error)
255305
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import * as fs from "fs/promises"
2+
import * as path from "path"
3+
import { ClineMessage } from "../../shared/ExtensionMessage"
4+
5+
export class ConversationSaver {
6+
private saveFolder: string
7+
private currentFilePath?: string
8+
9+
constructor(saveFolder: string) {
10+
this.saveFolder = saveFolder
11+
}
12+
13+
/**
14+
* Creates a new conversation file with the given messages
15+
* @param messages The messages to save
16+
* @returns The path to the created file
17+
*/
18+
async saveConversation(messages: ClineMessage[]): Promise<string> {
19+
try {
20+
console.log("Attempting to save conversation to folder:", this.saveFolder)
21+
22+
// Create save folder if it doesn't exist
23+
await fs.mkdir(this.saveFolder, { recursive: true })
24+
console.log("Save folder created/verified")
25+
26+
// Generate filename based on timestamp and first message
27+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
28+
const firstMessage = messages.find((m) => m.type === "say" && m.say === "task")
29+
const taskText = firstMessage?.text?.slice(0, 50).replace(/[^a-zA-Z0-9]/g, "-") || "conversation"
30+
const filename = `${timestamp}-${taskText}.json`
31+
console.log("Generated filename:", filename)
32+
33+
// Save to file
34+
this.currentFilePath = path.join(this.saveFolder, filename)
35+
console.log("Attempting to write to:", this.currentFilePath)
36+
37+
await fs.writeFile(
38+
this.currentFilePath,
39+
JSON.stringify({ messages, lastUpdated: new Date().toISOString() }, null, 2),
40+
"utf-8",
41+
)
42+
console.log("Successfully wrote file")
43+
44+
return this.currentFilePath
45+
} catch (error) {
46+
console.error("Error saving conversation:", error)
47+
console.error("Error details:", error instanceof Error ? error.message : String(error))
48+
console.error("Save folder was:", this.saveFolder)
49+
console.error("Current working directory:", process.cwd())
50+
throw new Error("Failed to save conversation")
51+
}
52+
}
53+
54+
/**
55+
* Updates an existing conversation file with new messages
56+
* @param messages The updated messages to save
57+
* @returns The path to the updated file
58+
*/
59+
async updateConversation(messages: ClineMessage[]): Promise<string> {
60+
console.log("Attempting to update conversation")
61+
62+
if (!this.currentFilePath) {
63+
console.log("No current file path, creating new conversation file")
64+
return await this.saveConversation(messages)
65+
}
66+
67+
try {
68+
console.log("Updating existing file at:", this.currentFilePath)
69+
await fs.writeFile(
70+
this.currentFilePath,
71+
JSON.stringify({ messages, lastUpdated: new Date().toISOString() }, null, 2),
72+
"utf-8",
73+
)
74+
console.log("Successfully updated conversation file")
75+
return this.currentFilePath
76+
} catch (error) {
77+
console.error("Error updating conversation:", error)
78+
console.error("Error details:", error instanceof Error ? error.message : String(error))
79+
console.error("Current file path was:", this.currentFilePath)
80+
console.error("Current working directory:", process.cwd())
81+
throw new Error("Failed to update conversation")
82+
}
83+
}
84+
85+
/**
86+
* Gets the current conversation file path
87+
*/
88+
getCurrentFilePath(): string | undefined {
89+
return this.currentFilePath
90+
}
91+
92+
/**
93+
* Updates the save folder path
94+
* @param newPath The new save folder path
95+
*/
96+
updateSaveFolder(newPath: string) {
97+
this.saveFolder = newPath
98+
// Reset current file path since we're changing folders
99+
this.currentFilePath = undefined
100+
}
101+
}

0 commit comments

Comments
 (0)