Skip to content

Commit fa97aa0

Browse files
committed
Save to markdown instead of json
1 parent 208e645 commit fa97aa0

File tree

4 files changed

+114
-22
lines changed

4 files changed

+114
-22
lines changed

src/core/Cline.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,24 @@ type UserContent = Array<
7474

7575
export class Cline {
7676
readonly taskId: string
77-
api: ApiHandler
77+
private _api: ApiHandler
78+
private _apiProvider: string = "anthropic" // Default to anthropic as fallback
79+
80+
get api(): ApiHandler {
81+
return this._api
82+
}
83+
84+
set api(newApi: ApiHandler) {
85+
this._api = newApi
86+
}
87+
88+
get apiProvider(): string {
89+
return this._apiProvider
90+
}
91+
92+
set apiProvider(provider: string) {
93+
this._apiProvider = provider
94+
}
7895
private terminalManager: TerminalManager
7996
private urlContentFetcher: UrlContentFetcher
8097
private browserSession: BrowserSession
@@ -121,12 +138,15 @@ export class Cline {
121138
historyItem?: HistoryItem | undefined,
122139
experiments?: Record<string, boolean>,
123140
) {
141+
this._api = buildApiHandler(apiConfiguration)
142+
this._apiProvider = apiConfiguration.apiProvider ?? "anthropic"
124143
if (!task && !images && !historyItem) {
125144
throw new Error("Either historyItem or task/images must be provided")
126145
}
127146

128147
this.taskId = crypto.randomUUID()
129148
this.api = buildApiHandler(apiConfiguration)
149+
this.apiProvider = apiConfiguration.apiProvider ?? "anthropic"
130150
this.terminalManager = new TerminalManager()
131151
this.urlContentFetcher = new UrlContentFetcher(provider.context)
132152
this.browserSession = new BrowserSession(provider.context)
@@ -2726,11 +2746,18 @@ export class Cline {
27262746

27272747
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
27282748
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
2749+
const model = this.api.getModel()
2750+
const apiInfo = {
2751+
provider: this.apiProvider,
2752+
model: model.id,
2753+
}
2754+
27292755
await this.say(
27302756
"api_req_started",
27312757
JSON.stringify({
27322758
request:
27332759
userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n") + "\n\nLoading...",
2760+
...apiInfo,
27342761
}),
27352762
)
27362763

@@ -2745,6 +2772,7 @@ export class Cline {
27452772
const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
27462773
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
27472774
request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
2775+
...apiInfo,
27482776
} satisfies ClineApiReqInfo)
27492777
await this.saveClineMessages()
27502778
await this.providerRef.deref()?.postStateToWebview()
@@ -2760,6 +2788,11 @@ export class Cline {
27602788
// fortunately api_req_finished was always parsed out for the gui anyways, so it remains solely for legacy purposes to keep track of prices in tasks from history
27612789
// (it's worth removing a few months from now)
27622790
const updateApiReqMsg = (cancelReason?: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
2791+
const model = this.api.getModel()
2792+
const apiInfo = {
2793+
provider: this.apiProvider,
2794+
model: model.id,
2795+
}
27632796
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
27642797
...JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}"),
27652798
tokensIn: inputTokens,
@@ -2768,15 +2801,10 @@ export class Cline {
27682801
cacheReads: cacheReadTokens,
27692802
cost:
27702803
totalCost ??
2771-
calculateApiCost(
2772-
this.api.getModel().info,
2773-
inputTokens,
2774-
outputTokens,
2775-
cacheWriteTokens,
2776-
cacheReadTokens,
2777-
),
2804+
calculateApiCost(model.info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens),
27782805
cancelReason,
27792806
streamingFailedMessage,
2807+
...apiInfo,
27802808
} satisfies ClineApiReqInfo)
27812809
}
27822810

src/core/conversation-saver/index.ts

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,68 @@ export class ConversationSaver {
1010
this.saveFolder = saveFolder
1111
}
1212

13+
private formatMessagesAsMarkdown(messages: ClineMessage[]): string {
14+
const lines: string[] = []
15+
lines.push(`# Conversation saved at ${new Date().toISOString()}\n`)
16+
17+
for (const msg of messages) {
18+
const timestamp = new Date(msg.ts).toLocaleTimeString()
19+
20+
if (msg.type === "say") {
21+
if (msg.say === "task") {
22+
lines.push(`## Task (${timestamp})`)
23+
if (msg.text) lines.push(msg.text)
24+
} else if (msg.say === "text") {
25+
lines.push(`### Assistant (${timestamp})`)
26+
if (msg.text) lines.push(msg.text)
27+
} else if (msg.say === "user_feedback") {
28+
lines.push(`### User Feedback (${timestamp})`)
29+
if (msg.text) lines.push(msg.text)
30+
} else if (msg.say === "error") {
31+
lines.push(`### Error (${timestamp})`)
32+
if (msg.text) lines.push(`\`\`\`\n${msg.text}\n\`\`\``)
33+
} else if (msg.say === "api_req_started") {
34+
try {
35+
const apiInfo = JSON.parse(msg.text || "{}")
36+
if (apiInfo.tokensIn || apiInfo.tokensOut || apiInfo.cost) {
37+
lines.push(`### API Usage (${timestamp})`)
38+
if (apiInfo.provider) lines.push(`- Provider: ${apiInfo.provider}`)
39+
if (apiInfo.model) lines.push(`- Model: ${apiInfo.model}`)
40+
if (apiInfo.tokensIn) lines.push(`- Input tokens: ${apiInfo.tokensIn}`)
41+
if (apiInfo.tokensOut) lines.push(`- Output tokens: ${apiInfo.tokensOut}`)
42+
if (apiInfo.cost) lines.push(`- Cost: $${apiInfo.cost.toFixed(6)}`)
43+
}
44+
} catch (e) {
45+
// Skip malformed JSON
46+
}
47+
}
48+
} else if (msg.type === "ask") {
49+
if (msg.ask === "followup") {
50+
lines.push(`### Question (${timestamp})`)
51+
if (msg.text) lines.push(msg.text)
52+
} else if (msg.ask === "command") {
53+
lines.push(`### Command (${timestamp})`)
54+
if (msg.text) lines.push(`\`\`\`bash\n${msg.text}\n\`\`\``)
55+
} else if (msg.ask === "command_output") {
56+
lines.push(`### Command Output (${timestamp})`)
57+
if (msg.text) lines.push(`\`\`\`\n${msg.text}\n\`\`\``)
58+
}
59+
}
60+
61+
// Handle images if present
62+
if (msg.images && msg.images.length > 0) {
63+
lines.push("\n**Images:**")
64+
msg.images.forEach((img, i) => {
65+
lines.push(`![Image ${i + 1}](${img})`)
66+
})
67+
}
68+
69+
lines.push("") // Add blank line between messages
70+
}
71+
72+
return lines.join("\n")
73+
}
74+
1375
/**
1476
* Creates a new conversation file with the given messages
1577
* @param messages The messages to save
@@ -27,18 +89,15 @@ export class ConversationSaver {
2789
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
2890
const firstMessage = messages.find((m) => m.type === "say" && m.say === "task")
2991
const taskText = firstMessage?.text?.slice(0, 50).replace(/[^a-zA-Z0-9]/g, "-") || "conversation"
30-
const filename = `${timestamp}-${taskText}.json`
92+
const filename = `${timestamp}-${taskText}.md`
3193
console.log("Generated filename:", filename)
3294

3395
// Save to file
3496
this.currentFilePath = path.join(this.saveFolder, filename)
3597
console.log("Attempting to write to:", this.currentFilePath)
3698

37-
await fs.writeFile(
38-
this.currentFilePath,
39-
JSON.stringify({ messages, lastUpdated: new Date().toISOString() }, null, 2),
40-
"utf-8",
41-
)
99+
const markdown = this.formatMessagesAsMarkdown(messages)
100+
await fs.writeFile(this.currentFilePath, markdown, "utf-8")
42101
console.log("Successfully wrote file")
43102

44103
return this.currentFilePath
@@ -66,11 +125,8 @@ export class ConversationSaver {
66125

67126
try {
68127
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-
)
128+
const markdown = this.formatMessagesAsMarkdown(messages)
129+
await fs.writeFile(this.currentFilePath, markdown, "utf-8")
74130
console.log("Successfully updated conversation file")
75131
return this.currentFilePath
76132
} catch (error) {

src/core/webview/ClineProvider.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,7 +1463,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
14631463
await this.storeSecret("unboundApiKey", unboundApiKey)
14641464
await this.updateGlobalState("unboundModelId", unboundModelId)
14651465
if (this.cline) {
1466-
this.cline.api = buildApiHandler(apiConfiguration)
1466+
const newApi = buildApiHandler(apiConfiguration)
1467+
this.cline.api = newApi
1468+
this.cline.apiProvider = apiConfiguration.apiProvider ?? "anthropic"
14671469
}
14681470
}
14691471

@@ -1594,7 +1596,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
15941596
await this.storeSecret("openRouterApiKey", apiKey)
15951597
await this.postStateToWebview()
15961598
if (this.cline) {
1597-
this.cline.api = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
1599+
const newApi = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
1600+
this.cline.api = newApi
1601+
this.cline.apiProvider = openrouter
15981602
}
15991603
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
16001604
}
@@ -1626,10 +1630,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
16261630
await this.storeSecret("glamaApiKey", apiKey)
16271631
await this.postStateToWebview()
16281632
if (this.cline) {
1629-
this.cline.api = buildApiHandler({
1633+
const newApi = buildApiHandler({
16301634
apiProvider: glama,
16311635
glamaApiKey: apiKey,
16321636
})
1637+
this.cline.api = newApi
1638+
this.cline.apiProvider = glama
16331639
}
16341640
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
16351641
}

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ export interface ClineApiReqInfo {
218218
cost?: number
219219
cancelReason?: ClineApiReqCancelReason
220220
streamingFailedMessage?: string
221+
provider?: string
222+
model?: string
221223
}
222224

223225
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"

0 commit comments

Comments
 (0)